Python JSON serializza un oggetto Decimale


242

Ho Decimal('3.9')come parte di un oggetto e desidero codificarlo in una stringa JSON che dovrebbe apparire come {'x': 3.9}. Non mi interessa la precisione sul lato client, quindi un galleggiante va bene.

C'è un buon modo per serializzare questo? JSONDecoder non accetta oggetti decimali e la conversione in float produce in anticipo un {'x': 3.8999999999999999}errore, e sarà un grande spreco di larghezza di banda.



3.8999999999999999 non è più sbagliato di 3.4. 0.2 non ha una rappresentazione float esatta.
Jasen,

@Jasen 3.89999999999 è circa il 12,8% più sbagliato di 3.4. Lo standard JSON riguarda solo la serializzazione e la notazione, non l'implementazione. L'uso di IEEE754 non fa parte delle specifiche JSON non elaborate, è solo il modo più comune per implementarlo. Un'implementazione che utilizza solo un'aritmetica decimale precisa è completamente (in effetti, anche più rigorosamente) conforme.
hraban

😂 meno sbagliato. ironico.
hraban

Risposte:


147

Che ne dici di una sottoclasse json.JSONEncoder?

class DecimalEncoder(json.JSONEncoder):
    def _iterencode(self, o, markers=None):
        if isinstance(o, decimal.Decimal):
            # wanted a simple yield str(o) in the next line,
            # but that would mean a yield on the line with super(...),
            # which wouldn't work (see my comment below), so...
            return (str(o) for o in [o])
        return super(DecimalEncoder, self)._iterencode(o, markers)

Quindi usalo così:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

Ahi, ho appena notato che in realtà non funzionerà in questo modo. Modificherà di conseguenza. (L'idea rimane la stessa.)
Michał Marczyk il

Il problema era che DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()restituiva il corretto '3.9', ma DecimalEncoder()._iterencode(3.9).next()restituiva un oggetto generatore che sarebbe tornato solo '3.899...'quando si era accumulato su un altro .next(). Generatore di affari divertenti. Oh bene ... Dovrebbe funzionare ora.
Michał Marczyk,

8
Non puoi semplicemente return (str(o),)invece? [o]è un elenco con solo 1 elemento, perché preoccuparsi di passarci sopra?
Aprire il

2
@Mark: return (str(o),)restituirebbe una tupla di lunghezza 1, mentre il codice nella risposta restituisce un generatore di lunghezza 1. Vedi i documenti iterencode ()
Abgan,

30
Questa implementazione non funziona più. Quello di Elias Zamaria è quello che lavora sullo stesso stile.
piro,

224

Simplejson 2.1 e versioni successive hanno il supporto nativo per il tipo Decimale:

>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'

Si noti che use_decimalè Trueper impostazione predefinita:

def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
    allow_nan=True, cls=None, indent=None, separators=None,
    encoding='utf-8', default=None, use_decimal=True,
    namedtuple_as_object=True, tuple_as_array=True,
    bigint_as_string=False, sort_keys=False, item_sort_key=None,
    for_json=False, ignore_nan=False, **kw):

Così:

>>> json.dumps(Decimal('3.9'))
'3.9'

Speriamo che questa funzione sia inclusa nella libreria standard.


7
Hmm, per me questo converte oggetti decimali in float, il che non è accettabile. Perdita di precisione quando si lavora con la valuta, ad esempio.
Matthew Schinckel,

12
@MatthewSchinckel Penso che non lo sia. In realtà ne ricava una stringa. E se si json.loads(s, use_decimal=True)inserisce nuovamente la stringa risultante , si restituisce il decimale. Nessun galleggiante nell'intero processo. Modificato sopra la risposta. Spero che il poster originale sia perfetto.
Shekhar,

1
Aha, penso di non usare anche use_decimal=Truei carichi.
Matthew Schinckel,

1
Per me json.dumps({'a' : Decimal('3.9')}, use_decimal=True)'{"a": 3.9}'. L'obiettivo non era '{"a": "3.9"}'?
MrJ,

5
simplejson.dumps(decimal.Decimal('2.2'))funziona anche: non esplicito use_decimal(testato su simplejson / 3.6.0). Un altro modo per json.loads(s, parse_float=Decimal)ricaricarlo è: cioè, puoi leggerlo usando stdlib json(e simplejsonsono supportate anche le versioni precedenti ).
jfs,

181

Vorrei far sapere a tutti che ho provato la risposta di Michał Marczyk sul mio server Web che eseguiva Python 2.6.5 e che ha funzionato bene. Tuttavia, ho eseguito l'aggiornamento a Python 2.7 e ha smesso di funzionare. Ho provato a pensare a un modo per codificare gli oggetti decimali e questo è quello che mi è venuto in mente:

import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super(DecimalEncoder, self).default(o)

Si spera che ciò possa aiutare chiunque abbia problemi con Python 2.7. L'ho provato e sembra funzionare bene. Se qualcuno nota qualche bug nella mia soluzione o trova un modo migliore, per favore fatemelo sapere.


4
Python 2.7 ha cambiato le regole per arrotondare i float in modo che funzioni. Vedi discussione in stackoverflow.com/questions/1447287/…
Nelson,

2
Per quelli di noi che non possono usare simplejson (ad es. Su Google App Engine) questa risposta è una manna dal cielo.
Joel Cross,

17
Utilizzare unicodeo strinvece di floatper garantire la precisione.
Seppo Erviälä,

2
Il problema con 54.3999 ... era importante in Python 2.6.xe versioni precedenti in cui la conversione float in stringa non funzionava regolarmente, ma la conversione Decimale in str è molto più errata perché verrebbe serializzata come stringa con virgolette doppie "54.4", non come un numero.
hynekcer,

1
Funziona in python3
SeanFromIT il

43

Nella mia app Flask, che utilizza python 2.7.11, l'alchimia di flask (con tipi 'db.decimal') e Flask Marshmallow (per serializzatore e deserializzatore 'istantanei), ho riscontrato questo errore, ogni volta che ho fatto GET o POST . Il serializzatore e il deserializzatore non sono riusciti a convertire i tipi decimali in qualsiasi formato identificabile JSON.

Ho fatto un "pip install simplejson", quindi solo aggiungendo

import simplejson as json

il serializzatore e il deserializzatore iniziano a fare le fusa di nuovo. Non ho fatto nient'altro ... DEciamls viene visualizzato come formato float "234.00".


1
la soluzione più semplice
SMDC,

1
Stranamente, non è nemmeno necessario importare simplejson, basta installarlo per risolvere il problema. Inizialmente menzionato da questa risposta .
bsplosion,

Questo non funziona su di me e lo ha ancora ottenuto Decimal('0.00') is not JSON serializable dopo averlo installato tramite pip. Questa situazione si verifica quando si utilizzano sia marshmallow che grafene. Quando una query viene chiamata su un'API di riposo, marshmallow funziona in modo previsto per i campi decimali. Tuttavia quando viene chiamato con graphql ha generato un is not JSON serializableerrore.
Roel

Fantastico, superbo,
Uomo Ragno

Perfetto! Funziona in situazioni in cui stai utilizzando un modulo scritto da qualcun altro che non puoi modificare facilmente (nel mio caso gspread per l'utilizzo di Fogli Google)
happyskeptic

32

Ho provato a passare da simplejson a builtin json per GAE 2.7 e ho avuto problemi con il decimale. Se il valore predefinito restituiva str (o) c'erano delle virgolette (perché _iterencode chiama _iterencode sui risultati di default), e float (o) rimuoveva lo 0 finale.

Se il valore predefinito restituisce un oggetto di una classe che eredita da float (o tutto ciò che chiama repr senza ulteriore formattazione) e ha un metodo __repr__ personalizzato, sembra funzionare come voglio.

import json
from decimal import Decimal

class fakefloat(float):
    def __init__(self, value):
        self._value = value
    def __repr__(self):
        return str(self._value)

def defaultencode(o):
    if isinstance(o, Decimal):
        # Subclass float with custom repr?
        return fakefloat(o)
    raise TypeError(repr(o) + " is not JSON serializable")

json.dumps([10.20, "10.20", Decimal('10.20')], default=defaultencode)
'[10.2, "10.20", 10.20]'

Bello! Questo si assicura che il valore decimale finisca in JSON come float Javascript, senza che Python lo arrotoli per primo al valore float più vicino.
Konrad

3
Purtroppo questo non funziona negli ultimi Python 3. Esiste ora un codice di percorso rapido che considera tutte le sottoclassi float come float e non chiama del tutto repr su di esse.
Antti Haapala,

@AnttiHaapala, l'esempio funziona bene su Python 3.6.
Cristian Ciupitu,

@CristianCiupitu in effetti, non mi sembra di riuscire a riprodurre il cattivo comportamento adesso
Antti Haapala,

2
La soluzione ha smesso di funzionare dalla v3.5.2rc1, consultare github.com/python/cpython/commit/… . C'è float.__repr__hardcoded (che perde precisione) e fakefloat.__repr__non viene chiamato affatto. La soluzione sopra funziona correttamente per python3 fino alla 3.5.1, se fakefloat ha un metodo aggiuntivo def __float__(self): return self.
miroslav,

30

Manca l'opzione nativa, quindi la aggiungerò per il prossimo / a ragazzo che la cerca.

A partire da Django 1.7.x c'è un built-in DjangoJSONEncoderda cui puoi ottenerlo django.core.serializers.json.

import json
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict

model_instance = YourModel.object.first()
model_dict = model_to_dict(model_instance)

json.dumps(model_dict, cls=DjangoJSONEncoder)

Presto!


Anche se questo è fantastico da sapere, l'OP non ha chiesto di Django?
std''OrgnlDave,

4
@ std''OrgnlDave hai ragione al 100%. Ho dimenticato come sono arrivato qui, ma ho cercato su google questa domanda con "django" allegato al termine di ricerca e questo è venuto fuori, dopo un po 'più google, ho trovato la risposta e l'ho aggiunta qui per la prossima persona come me, che si imbatte in esso
Javier Buzzi

6
mi salvi il giorno
gaozhidf,

14

I miei $ 0,02!

Estendo un sacco di codificatore JSON dal momento che sto serializzando tonnellate di dati per il mio server web. Ecco un bel codice. Nota che è facilmente estendibile praticamente a qualsiasi formato di dati che ti piace e riprodurrà 3.9 come"thing": 3.9

JSONEncoder_olddefault = json.JSONEncoder.default
def JSONEncoder_newdefault(self, o):
    if isinstance(o, UUID): return str(o)
    if isinstance(o, datetime): return str(o)
    if isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o))
    if isinstance(o, decimal.Decimal): return str(o)
    return JSONEncoder_olddefault(self, o)
json.JSONEncoder.default = JSONEncoder_newdefault

Rende la mia vita molto più semplice ...


3
Questo non è corretto: riprodurrà 3.9 come "thing": "3.9".
Glifo

le migliori soluzioni di tutte, molto semplici, grazie mi hai salvato la giornata, per me è sufficiente salvare il numero, in stringa per i decimali è ok
stackdave

@Glyph tramite standard JSON (di cui ce ne sono alcuni ...), un numero non quotato è un virgola mobile a precisione doppia, non un numero decimale. La citazione è l'unico modo per garantire la compatibilità.
std''OrgnlDave,

2
hai una citazione per questo? Ogni specifica che ho letto implica che dipende dall'implementazione.
Glifo

12

3.9non può essere rappresentato esattamente nei float IEEE, verrà sempre come 3.8999999999999999, ad esempio provare print repr(3.9), puoi leggere di più qui:

http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html

Quindi, se non vuoi float, l'unica opzione devi inviarlo come stringa e per consentire la conversione automatica degli oggetti decimali in JSON, fai qualcosa del genere:

import decimal
from django.utils import simplejson

def json_encode_decimal(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)
    raise TypeError(repr(obj) + " is not JSON serializable")

d = decimal.Decimal('3.5')
print simplejson.dumps([d], default=json_encode_decimal)

So che non sarà 3.9 internamente una volta analizzato sul client, ma 3.9 è un float JSON valido. cioè, json.loads("3.9")funzionerà, e vorrei che fosse questo
Knio il

@Anurag Nel tuo esempio intendevi repr (obj) anziché repr (o).
Orokusaki,

Non morirà se provi a codificare qualcosa che non è decimale?
mikemaccana,

1
@nailer, no non puoi, puoi provarlo, perché la ragione è predefinita solleva un'eccezione per segnalare che dovrebbe essere usato il gestore successivo
Anurag Uniyal

1
Vedi la risposta di mikez302: in Python 2.7 o versioni successive, questo non si applica più.
Joel Cross,

9

Per gli utenti di Django :

Di recente mi sono imbattuto TypeError: Decimal('2337.00') is not JSON serializable mentre la codifica JSON vale a direjson.dumps(data)

Soluzione :

# converts Decimal, Datetime, UUIDs to str for Encoding
from django.core.serializers.json import DjangoJSONEncoder  

json.dumps(response.data, cls=DjangoJSONEncoder)

Ma ora il valore Decimale sarà una stringa, ora possiamo impostare esplicitamente il parser del valore decimale / float durante la decodifica dei dati, usando l' parse_floatopzione in json.loads:

import decimal 

data = json.loads(data, parse_float=decimal.Decimal) # default is float(num_str)

8

Dal documento standard JSON , come collegato in json.org :

JSON è agnostico sulla semantica dei numeri. In qualsiasi linguaggio di programmazione, ci possono essere una varietà di tipi numerici di varie capacità e complementi, fissi o mobili, binari o decimali. Ciò può rendere difficile l'interscambio tra diversi linguaggi di programmazione. JSON invece offre solo la rappresentazione di numeri che gli umani usano: una sequenza di cifre. Tutti i linguaggi di programmazione sanno dare un senso alle sequenze di cifre anche se non sono d'accordo sulle rappresentazioni interne. Questo è sufficiente per consentire l'interscambio.

Quindi in realtà è accurato rappresentare i decimali come numeri (anziché come stringhe) in JSON. Muggito sta una possibile soluzione al problema.

Definire un codificatore JSON personalizzato:

import json


class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, Decimal):
            return float(obj)
        return super(CustomJsonEncoder, self).default(obj)

Quindi utilizzalo durante la serializzazione dei dati:

json.dumps(data, cls=CustomJsonEncoder)

Come notato dai commenti sulle altre risposte, le versioni precedenti di Python potrebbero rovinare la rappresentazione durante la conversione in float, ma non è più così.

Per recuperare il decimale in Python:

Decimal(str(value))

Questa soluzione è suggerita nella documentazione di Python 3.0 sui decimali :

Per creare un decimale da un float, prima convertirlo in una stringa.


2
Questo non è "fisso" in Python 3. Conversione a un float necessariamente ti fa perdere la rappresentazione decimale, e sarà portare a discrepanze. Se Decimalè importante usare, penso che sia meglio usare le stringhe.
juanpa.arrivillaga,

Credo che sia sicuro farlo da Python 3.1. La perdita di precisione potrebbe essere dannosa nelle operazioni aritmetiche, ma nel caso della codifica JSON, si sta semplicemente producendo una visualizzazione stringa del valore, quindi la precisione è più che sufficiente per la maggior parte dei casi d'uso. Tutto in JSON è già una stringa, quindi inserire virgolette attorno al valore non fa che sfidare le specifiche JSON.
Hugo Mota,

Detto questo, capisco le preoccupazioni relative alla conversione in float. Probabilmente esiste una strategia diversa da utilizzare con l'encoder per produrre la stringa di visualizzazione desiderata. Tuttavia, non penso che valga la pena produrre un valore quotato.
Hugo Mota,

@HugoMota "Tutto in JSON è già una stringa, quindi inserire virgolette attorno al valore non fa che sfidare le specifiche JSON." No: rfc-editor.org/rfc/rfc8259.txt - JSON è un formato di codifica basato su testo, ma ciò non significa che tutto ciò che deve essere interpretato come una stringa. La specifica definisce come codificare i numeri, separatamente dalle stringhe.
Gunnar Magnór Magnússon,

@ GunnarÞórMagnússon "JSON è un formato di codifica basato su testo" - questo è ciò che intendevo con "tutto è una stringa". La conversione anticipata dei numeri in stringa non manterrà magicamente la precisione poiché sarà comunque una stringa quando diventa JSON. E secondo le specifiche, i numeri non hanno virgolette attorno. È responsabilità del lettore preservare la precisione durante la lettura (non una citazione, solo la mia opinione su di essa).
Hugo Mota,

6

Questo è quello che ho estratto dalla nostra classe

class CommonJSONEncoder(json.JSONEncoder):

    """
    Common JSON Encoder
    json.dumps(myString, cls=CommonJSONEncoder)
    """

    def default(self, obj):

        if isinstance(obj, decimal.Decimal):
            return {'type{decimal}': str(obj)}

class CommonJSONDecoder(json.JSONDecoder):

    """
    Common JSON Encoder
    json.loads(myString, cls=CommonJSONEncoder)
    """

    @classmethod
    def object_hook(cls, obj):
        for key in obj:
            if isinstance(key, six.string_types):
                if 'type{decimal}' == key:
                    try:
                        return decimal.Decimal(obj[key])
                    except:
                        pass

    def __init__(self, **kwargs):
        kwargs['object_hook'] = self.object_hook
        super(CommonJSONDecoder, self).__init__(**kwargs)

Che passa unittest:

def test_encode_and_decode_decimal(self):
    obj = Decimal('1.11')
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': Decimal('1.11')}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': {'abc': Decimal('1.11')}}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

json.loads(myString, cls=CommonJSONEncoder)il commento dovrebbe esserejson.loads(myString, cls=CommonJSONDecoder)
Can Kavaklıoğlu il

object_hook richiede un valore di ritorno predefinito se obj non è decimale.
Can Kavaklıoğlu,

3

È possibile creare un codificatore JSON personalizzato secondo le proprie esigenze.

import json
from datetime import datetime, date
from time import time, struct_time, mktime
import decimal

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return str(o)
        if isinstance(o, date):
            return str(o)
        if isinstance(o, decimal.Decimal):
            return float(o)
        if isinstance(o, struct_time):
            return datetime.fromtimestamp(mktime(o))
        # Any other serializer if needed
        return super(CustomJSONEncoder, self).default(o)

Il decodificatore può essere chiamato in questo modo,

import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)

e l'output sarà:

>>'{"x": 3.9}'

fantastico ... Grazie per una soluzione di arresto (y)
muhammed basil

Funziona davvero! Grazie per aver condiviso la tua soluzione
tthreetorch il

3

Per coloro che non vogliono usare una libreria di terze parti ... Un problema con la risposta di Elias Zamaria è che si converte in float, il che può incorrere in problemi. Per esempio:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 1e-07}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01733}'

Il JSONEncoder.encode()metodo consente di restituire il contenuto json letterale, a differenza di quello JSONEncoder.default()che restituisce un tipo compatibile json (come float) che viene quindi codificato in modo normale. Il problema encode()è che (normalmente) funziona solo al livello più alto. Ma è ancora utilizzabile, con un po 'di lavoro extra (python 3.x):

import json
from collections.abc import Mapping, Iterable
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, Mapping):
            return '{' + ', '.join(f'{self.encode(k)}: {self.encode(v)}' for (k, v) in obj.items()) + '}'
        if isinstance(obj, Iterable) and (not isinstance(obj, str)):
            return '[' + ', '.join(map(self.encode, obj)) + ']'
        if isinstance(obj, Decimal):
            return f'{obj.normalize():f}'  # using normalize() gets rid of trailing 0s, using ':f' prevents scientific notation
        return super().encode(obj)

Che ti dà:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 0.0000001}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01734}'

2

Sulla base della risposta stdOrgnlDave ho definito questo wrapper che può essere chiamato con tipi opzionali in modo che l'encoder funzionerà solo per determinati tipi all'interno dei tuoi progetti. Credo che il lavoro dovrebbe essere svolto all'interno del codice e non utilizzare questo codificatore "predefinito" poiché "è meglio esplicito che implicito", ma capisco che l'uso di questo ti farà risparmiare un po 'di tempo. :-)

import time
import json
import decimal
from uuid import UUID
from datetime import datetime

def JSONEncoder_newdefault(kind=['uuid', 'datetime', 'time', 'decimal']):
    '''
    JSON Encoder newdfeault is a wrapper capable of encoding several kinds
    Use it anywhere on your code to make the full system to work with this defaults:
        JSONEncoder_newdefault()  # for everything
        JSONEncoder_newdefault(['decimal'])  # only for Decimal
    '''
    JSONEncoder_olddefault = json.JSONEncoder.default

    def JSONEncoder_wrapped(self, o):
        '''
        json.JSONEncoder.default = JSONEncoder_newdefault
        '''
        if ('uuid' in kind) and isinstance(o, uuid.UUID):
            return str(o)
        if ('datetime' in kind) and isinstance(o, datetime):
            return str(o)
        if ('time' in kind) and isinstance(o, time.struct_time):
            return datetime.fromtimestamp(time.mktime(o))
        if ('decimal' in kind) and isinstance(o, decimal.Decimal):
            return str(o)
        return JSONEncoder_olddefault(self, o)
    json.JSONEncoder.default = JSONEncoder_wrapped

# Example
if __name__ == '__main__':
    JSONEncoder_newdefault()

0

Se vuoi passare un dizionario contenente decimali alla requestslibreria (usando l' jsonargomento keyword), devi semplicemente installare simplejson:

$ pip3 install simplejson    
$ python3
>>> import requests
>>> from decimal import Decimal
>>> # This won't error out:
>>> requests.post('https://www.google.com', json={'foo': Decimal('1.23')})

Il motivo del problema è che requestsutilizza simplejsonsolo se è presente e ricade nell'integrato jsonse non è installato.


-6

questo può essere fatto aggiungendo

    elif isinstance(o, decimal.Decimal):
        yield str(o)

dentro \Lib\json\encoder.py:JSONEncoder._iterencode, ma speravo in una soluzione migliore


5
È possibile sottoclassare JSONEncoder come mostrato sopra, modificando i file Python installati di una libreria stabilita o l'interprete stesso dovrebbe essere l'ultima risorsa.
justanr,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.