Módulo json do Python, converte chaves de dicionário int em strings


130

Eu descobri que, quando o seguinte é executado, o módulo json do python (incluído desde o 2.6) converte as chaves do dicionário int em strings.

>>> import json
>>> releases = {1: "foo-v0.1"}
>>> json.dumps(releases)
'{"1": "foo-v0.1"}'

Existe alguma maneira fácil de preservar a chave como int, sem a necessidade de analisar a string no dump e no load. Acredito que seria possível usar os ganchos fornecidos pelo módulo json, mas novamente isso ainda requer análise. Existe algum argumento que eu tenha esquecido? Saúde, chaz

Sub-pergunta: Obrigado pelas respostas. Vendo como json funciona como eu temia, existe uma maneira fácil de transmitir o tipo de chave, talvez analisando a saída de despejos? Também devo observar que o código que faz o dumping e o código que baixa o objeto json de um servidor e o carrega são ambos escritos por mim.


23
chaves json tem que ser cadeias de caracteres
tonfa

Respostas:


86

Essa é uma daquelas diferenças sutis entre várias coleções de mapeamento que podem morder você. JSON trata as chaves como strings; O Python suporta chaves distintas, diferindo apenas no tipo.

No Python (e aparentemente em Lua), as chaves para um mapeamento (dicionário ou tabela, respectivamente) são referências a objetos. No Python, eles devem ser tipos imutáveis ​​ou objetos que implementam um __hash__método. (Os documentos de Lua sugerem que ele use automaticamente o ID do objeto como um hash / chave, mesmo para objetos mutáveis ​​e confie na internação de cadeias para garantir que cadeias equivalentes sejam mapeadas para os mesmos objetos).

No Perl, Javascript, awk e muitas outras linguagens, as chaves para hashes, matrizes associativas ou o que quer que sejam chamadas para o idioma especificado, são strings (ou "escalares" no Perl). Em perl $foo{1}, $foo{1.0}, and $foo{"1"}estão todas as referências ao mesmo mapeamento em %foo--- a chave é avaliada como um escalar!

JSON começou como uma tecnologia de serialização Javascript. (JSON meios J ava S cript O bject N otation.) Naturalmente que implementa semântica para a sua notação de mapeamento que são consistentes com a sua semântica de mapeamento.

Se as duas extremidades da sua serialização forem Python, é melhor usar pickles. Se você realmente precisar convertê-los novamente do JSON em objetos Python nativos, acho que você tem algumas opções. Primeiro, você pode tentar ( try: ... except: ...) converter qualquer chave em um número no caso de uma falha na pesquisa do dicionário. Como alternativa, se você adicionar código à outra extremidade (o serializador ou gerador desses dados JSON), poderá executar uma serialização JSON em cada um dos valores de chave - fornecendo-os como uma lista de chaves. (Em seguida, seu código Python primeiro itera sobre a lista de chaves, instanciando / desserializando-as em objetos Python nativos ... e depois os utiliza para acessar os valores do mapeamento).


1
Obrigado por isso. Infelizmente, não posso usar Pickle, mas sua ideia com a lista é ótima. Irá implementar isso agora, felicidades para a idéia.
Charles Ritchie

1
(Aliás, no Python 1, 1L (número inteiro longo) e 1.0 são mapeados para a mesma chave; mas "1" (uma sequência) não é mapeado para o mesmo que 1 (número inteiro) ou 1,0 (float) ou 1L (número inteiro longo) )
Jim Dennis

5
Seja cauteloso com a recomendação de usar Pickle. Pickle pode resultar em execução arbitrária de código; portanto, se a fonte dos dados que você está desserializando não for inerentemente confiável, você deve seguir um protocolo de serialização "seguro" como o JSON. Lembre-se também de que, à medida que o escopo dos projetos se expande, às vezes as funções que você esperava receberiam apenas informações confiáveis ​​começam a receber informações fornecidas pelo usuário, e as considerações de segurança nem sempre são revisadas.
AusIV

56

Não, não existe uma chave numérica no JavaScript. Todas as propriedades do objeto são convertidas em String.

var a= {1: 'a'};
for (k in a)
    alert(typeof k); // 'string'

Isso pode levar a alguns comportamentos curiosos:

a[999999999999999999999]= 'a'; // this even works on Array
alert(a[1000000000000000000000]); // 'a'
alert(a['999999999999999999999']); // fail
alert(a['1e+21']); // 'a'

Objetos JavaScript não são mapeamentos realmente adequados, como você entenderia em linguagens como Python, e o uso de chaves que não são String resulta em estranheza. É por isso que o JSON sempre escreve explicitamente chaves como seqüências de caracteres, mesmo quando não parece necessário.


1
Por que não é 999999999999999999999convertido para '999999999999999999999'?
Piotr Dobrogost

4
@PiotrDobrogost O JavaScript (como muitos idiomas) não pode armazenar números arbitrariamente grandes. O Numbertipo é um valor de ponto flutuante duplo IEEE 754 : você obtém 53 bits de mantissa, para que você possa armazenar até 2⁵³ (9007199254740992) com precisão inteira; além disso, os números inteiros serão arredondados para outros valores (portanto, 9007199254740993 === 9007199254740992). 999999999999999999999 arredonda para 10000000000000000000000000, para o qual a toStringrepresentação padrão é 1e+21.
quer

22

Como alternativa, você também pode tentar converter o dicionário para uma lista do formato [(k1, v1), (k2, v2)] enquanto codifica usando json e convertê-lo novamente no dicionário após decodificá-lo novamente.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Acredito que isso precisará de mais trabalho, como ter algum tipo de sinalizador para identificar quais parâmetros serão convertidos em dicionário depois de decodificá-lo novamente do json.


Boa solução para objetos dict sem objetos dict aninhados!
Tom Yu

15

Respondendo à sua subquestão:

Isso pode ser realizado usando json.loads(jsonDict, object_hook=jsonKeys2int)

def jsonKeys2int(x):
    if isinstance(x, dict):
            return {int(k):v for k,v in x.items()}
    return x

Essa função também funcionará para dicts aninhados e usa uma compreensão de dict.

Se você também deseja converter os valores, use:

def jsonKV2int(x):
    if isinstance(x, dict):
            return {int(k):(int(v) if isinstance(v, unicode) else v) for k,v in x.items()}
    return x

Que testa a instância dos valores e os lança apenas se forem objetos de strings (unicode para ser exato).

Ambas as funções assumem chaves (e valores) como inteiros.

Graças a:

Como usar if / else em uma compreensão de dicionário?

Converter uma chave de string em int em um dicionário


Isso foi ótimo. No meu caso, a decapagem não pode ser usada, por isso estou salvando as tripas de um objeto usando JSON via conversão em uma matriz de bytes, para que eu possa usar a compactação. Eu tenho chaves mistas, então eu só modificou o seu exemplo para ignorar um ValueError quando a chave não é conversível para um int
minillinim

11

Fui mordido pelo mesmo problema. Como outros já apontaram, no JSON, as chaves de mapeamento devem ser cadeias de caracteres. Você pode fazer uma das duas coisas. Você pode usar uma biblioteca JSON menos estrita, como demjson , que permite cadeias inteiras. Se nenhum outro programa (ou nenhum outro idioma) vai lê-lo, você deve ficar bem. Ou você pode usar uma linguagem de serialização diferente. Eu não sugeriria picles. É difícil de ler e não foi projetado para ser seguro . Em vez disso, sugiro o YAML, que é (quase) um superconjunto de JSON e permite chaves inteiras. (Pelo menos PyYAML faz.)


2

Converta o dicionário em string usando str(dict)e, em seguida, converta-o novamente em dict, fazendo o seguinte:

import ast
ast.literal_eval(string)

1

Aqui está a minha solução! Eu usei object_hook, é útil quando você aninhajson

>>> import json
>>> json_data = '{"1": "one", "2": {"-3": "minus three", "4": "four"}}'
>>> py_dict = json.loads(json_data, object_hook=lambda d: {int(k) if k.lstrip('-').isdigit() else k: v for k, v in d.items()})

>>> py_dict
{1: 'one', 2: {-3: 'minus three', 4: 'four'}}

Há filtro apenas para analisar a chave json em int. Você também pode usar o int(v) if v.lstrip('-').isdigit() else vfiltro para o valor json.


1

Fiz uma extensão muito simples da resposta de Murmel, que acho que funcionará em um dicionário bastante arbitrário (incluindo aninhado), supondo que ele possa ser descartado pelo JSON em primeiro lugar. Quaisquer chaves que possam ser interpretadas como números inteiros serão convertidas para int. Sem dúvida, isso não é muito eficiente, mas funciona para meus propósitos de armazenar e carregar a partir de strings json.

def convert_keys_to_int(d: dict):
    new_dict = {}
    for k, v in d.items():
        try:
            new_key = int(k)
        except ValueError:
            new_key = k
        if type(v) == dict:
            v = _convert_keys_to_int(v)
        new_dict[new_key] = v
    return new_dict

Supondo que todas as chaves no ditado original sejam números inteiros se puderem ser convertidas para int, isso retornará o dicionário original depois de armazenado como um json. por exemplo

>>>d = {1: 3, 2: 'a', 3: {1: 'a', 2: 10}, 4: {'a': 2, 'b': 10}}
>>>convert_keys_to_int(json.loads(json.dumps(d)))  == d
True

-1

Você pode escrever o seu json.dumpssozinho, aqui está um exemplo de djson : encoder.py . Você pode usá-lo assim:

assert dumps({1: "abc"}) == '{1: "abc"}'
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.