Formatar floats com módulo json padrão


100

Estou usando o módulo json padrão em python 2.6 para serializar uma lista de flutuadores. No entanto, estou obtendo resultados como este:

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

Eu quero que os flutuadores sejam formatados com apenas dois dígitos decimais. A saída deve ser semelhante a esta:

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

Tentei definir minha própria classe de codificador JSON:

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

Isso funciona para um único objeto flutuante:

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

Mas falha para objetos aninhados:

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

Não quero ter dependências externas, então prefiro ficar com o módulo json padrão.

Como posso conseguir isso?

Respostas:


80

Observação: isso não funciona em nenhuma versão recente do Python.

Infelizmente, acredito que você tenha que fazer isso corrigindo o macaco (o que, na minha opinião, indica um defeito de design no jsonpacote de biblioteca padrão ). Por exemplo, este código:

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

emite:

23.67
[23.67, 23.97, 23.87]

como você deseja. Obviamente, deve haver uma maneira arquitetada de sobrescrever de FLOAT_REPRforma que TODAS as representações de um float estejam sob seu controle, se você desejar; mas infelizmente não foi assim que o jsonpacote foi projetado :-(.


10
Esta solução não funciona no Python 2.7 usando a versão C do Python do codificador JSON.
Nelson,

25
Independentemente de como você fizer isso, use algo como% .15g ou% .12g em vez de% .3f.
Guido van Rossum

23
Eu encontrei este trecho no código de um programador júnior. Isso teria criado um bug muito sério, mas sutil se não tivesse sido detectado. Você pode colocar um aviso neste código explicando as implicações globais desse patching.
Rory Hart,

12
É uma boa higiene colocá-lo de volta quando terminar: 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
Como outros apontaram, isso não está mais funcionando pelo menos no Python 3.6+. Adicione alguns dígitos 23.67para ver como .2fnão é respeitado.
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])))

emite

[23.67, 23.97, 23.87]

Nenhum monkeypatching necessário.


2
Gosto desta solução; melhor integração e funciona com 2.7. Como estou criando os dados sozinho, eliminei a pretty_floatsfunção e simplesmente a integrei em meu outro código.
mikepurvis

1
Em Python3, fornece o erro "O objeto do mapa não é serializável em JSON" , mas você pode resolver convertendo o map () em uma lista comlist( map(pretty_floats, obj) )
Guglie

1
@Guglie: isso é porque no Python 3 mapretorna o iterador, não umlist
Azat Ibrakov

4
Não funciona para mim (Python 3.5.2, simplejson 3.16.0). Tentei com% .6g e [23.671234556, 23.971234556, 23.871234556], ele ainda imprime o número inteiro.
szali

27

Se você estiver usando o Python 2.7, uma solução simples é simplesmente arredondar seus flutuadores explicitamente para a precisão desejada.

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

Isso funciona porque o Python 2.7 tornou o arredondamento de flutuação mais consistente . Infelizmente, isso não funciona no 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'

As soluções mencionadas acima são soluções alternativas para o 2.6, mas nenhuma é totalmente adequada. O Monkey patching json.encoder.FLOAT_REPR não funciona se o tempo de execução do Python usa uma versão C do módulo JSON. A classe PrettyFloat na resposta de Tom Wuttke funciona, mas apenas se a codificação% g funcionar globalmente para seu aplicativo. O% .15g é um pouco mágico, ele funciona porque a precisão do float é de 17 dígitos significativos e% g não exibe zeros à direita.

Passei algum tempo tentando fazer um PrettyFloat que permitisse customização de precisão para cada número. Ou seja, uma sintaxe como

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

Não é fácil acertar. Herdar da flutuação é estranho. Herdar de Object e usar uma subclasse JSONEncoder com seu próprio método default () deve funcionar, exceto que o módulo json parece assumir que todos os tipos personalizados devem ser serializados como strings. Ou seja: você acaba com a string Javascript "0,33" na saída, não o número 0,33. Pode haver uma maneira de fazer isso funcionar, mas é mais difícil do que parece.


Outra abordagem para Python 2.6 usando JSONEncoder.iterencode e correspondência de padrões pode ser vista em github.com/migurski/LilJSON/blob/master/liljson.py
Nelson

Espero que isso torne a passagem de seus flutuadores mais leve - gosto de como podemos evitar bagunçar as classes JSON, o que pode ser uma merda.
Lincoln B

20

É uma pena que dumpsnão permita que você faça nada para flutuar. No entanto, loadssim. Portanto, se você não se importar com a carga extra da CPU, poderá jogá-la no codificador / decodificador / codificador e obter o resultado correto:

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

Obrigado, esta sugestão é realmente útil. Eu não sabia sobre o parse_floatkwarg!
Anônimo

A sugestão mais simples aqui que também funciona no 3.6.
Brent Faust

Observe a frase "não se preocupe com a carga extra da CPU". Definitivamente, não use essa solução se você tiver muitos dados para serializar. Para mim, adicionar isso sozinho fez um programa que faz um cálculo não trivial levar 3 vezes mais tempo.
Shaneb

11

Aqui está uma solução que funcionou para mim no Python 3 e não requer patching do macaco:

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

O resultado é:

[23.63, 23.93, 23.84]

Ele copia os dados, mas com flutuações arredondadas.


9

Se você estiver travado com o Python 2.5 ou versões anteriores: o truque do monkey-patch não parece funcionar com o módulo simplejson original se os speedups C estiverem instalados:

$ 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

Você pode fazer o que precisa, mas não está documentado:

>>> 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
Parece legal, mas parece não funcionar no Python 3.6. Em particular, não vi uma FLOAT_REPRconstante no json.encodermódulo.
Tomasz Gandor

2

A solução de Alex Martelli funcionará para aplicativos single threaded, mas pode não funcionar para aplicativos multi-threaded que precisam controlar o número de casas decimais por thread. Aqui está uma solução que deve funcionar em aplicativos multiencadeados:

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

Você pode simplesmente definir encoder.thread_local.decimal_places para o número de casas decimais que você deseja, e a próxima chamada para json.dumps () nesse segmento usará esse número de casas decimais


2

Se você precisar fazer isso no python 2.7 sem substituir o json.encoder.FLOAT_REPR global, aqui está uma maneira.

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)

Então, no python 2.7:

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

No python 2.6, ele não funciona exatamente como Matthew Schinckel aponta abaixo:

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

4
Parecem strings, não números.
Matthew Schinckel

1

Prós:

  • Funciona com qualquer codificador JSON, ou mesmo repr.
  • Curto (ish), parece funcionar.

Contras:

  • Hack de expressão regular feio, mal testado.
  • Complexidade quadrática.

    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

Ao importar o módulo json padrão, basta alterar o codificador padrão FLOAT_REPR. Não há realmente a necessidade de importar ou criar instâncias do 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]'

Às vezes, também é muito útil gerar como json a melhor representação que o python pode adivinhar com str. Isso garantirá que os dígitos significativos não sejam ignorados.

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

Concordo com @Nelson que herdar de float é estranho, mas talvez uma solução que apenas toque a __repr__função possa ser perdoada. Acabei usando o decimalpacote para reformatar os flutuadores quando necessário. A vantagem é que isso funciona em todos os contextos em que repr()está sendo chamado, então também ao simplesmente imprimir listas no stdout, por exemplo. Além disso, a precisão é configurável em tempo de execução, após a criação dos dados. A desvantagem é, claro, que seus dados precisam ser convertidos para esta classe especial de float (como infelizmente você não consegue fazer um monkey patch float.__repr__). Para isso, forneço uma breve função de conversão.

O código:

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

Exemplo de uso:

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

Isso não funciona com o pacote Python3 json embutido, que não usa __repr __ ().
Ian Goldby

0

Usando numpy

Se você realmente tiver flutuadores muito longos, poderá arredondá-los para cima / para baixo corretamente com 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]'


-1

Acabei de lançar o fjson , uma pequena biblioteca Python para corrigir esse problema. Instale com

pip install fjson

e usar apenas como json, com a adição do float_formatparâmetro:

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
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.