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?

Manuel Ceron
fonte

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 :-(.

Alex Martelli
fonte
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.

Tom Wuttke
fonte
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.

Nelson
fonte
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]'
Claude
fonte
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.

Jcoffland
fonte
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]'
>>> 
Carlos Valiente
fonte
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]'
Ned Batchelder
fonte
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

Anton I. Sipos
fonte
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"]'
Mike Fogel
fonte
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
Sam Watkins
fonte
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]'
F Pereira
fonte
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]]'
user1556435
fonte
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]'

Mikhail
fonte
-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
}
Nico Schlömer
fonte