Il modulo json di Python, converte le chiavi del dizionario int in stringhe


132

Ho scoperto che quando viene eseguito quanto segue, il modulo json di python (incluso dalla 2.6) converte le chiavi del dizionario int in stringhe.

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

Esiste un modo semplice per conservare la chiave come int, senza la necessità di analizzare la stringa durante il dump e il caricamento. Credo che sarebbe possibile utilizzare gli hook forniti dal modulo json, ma ancora una volta questo richiede ancora l'analisi. C'è forse un argomento che ho trascurato? applausi, chaz

Domanda secondaria: grazie per le risposte. Visto che json funziona come temevo, esiste un modo semplice per trasmettere il tipo di chiave magari analizzando l'output dei dump? Inoltre dovrei notare che il codice che esegue il dumping e il codice che scarica l'oggetto json da un server e lo carica, sono entrambi scritti da me.


23
le chiavi JSON devono essere stringhe
tonfa

Risposte:


87

Questa è una di quelle sottili differenze tra le varie raccolte di mappe che possono morderti. JSON tratta le chiavi come stringhe; Python supporta chiavi distinte che differiscono solo per tipo.

In Python (e apparentemente in Lua) le chiavi di una mappatura (dizionario o tabella, rispettivamente) sono riferimenti a oggetti. In Python devono essere tipi immutabili o devono essere oggetti che implementano un __hash__metodo. (I documenti di Lua suggeriscono che utilizza automaticamente l'ID dell'oggetto come hash / chiave anche per oggetti mutabili e si basa sull'internamento di stringhe per garantire che le stringhe equivalenti si mappino agli stessi oggetti).

In Perl, Javascript, awk e molti altri linguaggi le chiavi per hash, array associativi o qualunque cosa siano chiamati per la lingua data, sono stringhe (o "scalari" in Perl). In perl ci $foo{1}, $foo{1.0}, and $foo{"1"}sono tutti i riferimenti alla stessa mappatura in %foo--- la chiave è valutata come uno scalare!

JSON è iniziato come una tecnologia di serializzazione Javascript. (JSON sta per J ava S cripta O bject N flottazione.) Naturalmente implementa la semantica per la sua notazione di mappatura che siano coerenti con la sua semantica di mappatura.

Se entrambe le estremità della serializzazione saranno Python, faresti meglio a usare i sottaceti. Se hai davvero bisogno di riconvertirli da JSON in oggetti Python nativi, immagino che tu abbia un paio di scelte. Per prima cosa potresti provare ( try: ... except: ...) per convertire qualsiasi chiave in un numero in caso di errore di ricerca nel dizionario. In alternativa, se aggiungi codice all'altra estremità (il serializzatore o il generatore di questi dati JSON), potresti fare in modo che esegua una serializzazione JSON su ciascuno dei valori della chiave, fornendo quelli come un elenco di chiavi. (Quindi il tuo codice Python dovrebbe prima iterare l'elenco delle chiavi, istanziandole / deserializzandole in oggetti Python nativi ... e quindi usandoli per accedere ai valori fuori dalla mappatura).


1
Grazie per quello. Sfortunatamente non posso usare Pickle, ma la tua idea con l'elenco è fantastica. Lo implementerò ora, applausi per l'idea.
Charles Ritchie,

1
(Per inciso, in Python 1, 1L (intero lungo) e 1.0 mappano alla stessa chiave; ma "1" (una stringa) non corrisponde a 1 (intero) o 1.0 (float) o 1L (intero lungo ).
Jim Dennis

5
Sii cauto con la raccomandazione di usare Pickle. Pickle può comportare l'esecuzione di codice arbitrario, quindi se l'origine dei dati che stai deserializzando non è intrinsecamente affidabile, dovresti attenersi a un protocollo di serializzazione "sicuro" come JSON. Inoltre, tieni presente che con l'espansione dell'ambito dei progetti, a volte le funzioni che ti aspettavi avrebbero ricevuto solo input attendibili iniziano a ricevere l'input fornito dall'utente e le considerazioni sulla sicurezza non vengono sempre riviste.
AusIV

55

No, non esiste una chiave numerica in JavaScript. Tutte le proprietà degli oggetti vengono convertite in String.

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

Questo può portare ad alcuni comportamenti apparentemente curiosi:

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

Gli oggetti JavaScript non sono mappature veramente corrette come lo capiresti in linguaggi come Python e l'uso di chiavi che non sono String produce stranezze. Questo è il motivo per cui JSON scrive sempre esplicitamente le chiavi come stringhe, anche dove non sembra necessario.


1
Perché non viene 999999999999999999999convertito in '999999999999999999999'?
Piotr Dobrogost

4
@PiotrDobrogost JavaScript (come molte lingue) non può memorizzare numeri arbitrariamente grandi. Il Numbertipo è un doppio valore in virgola mobile IEEE 754 : ottieni 53 bit di mantissa, quindi puoi memorizzare fino a 2 up³ (9007199254740992) con precisione intera; oltre i numeri interi verranno arrotondati ad altri valori (quindi 9007199254740993 === 9007199254740992). 999999999999999999999 arrotonda a 1000000000000000000000, per il quale la toStringrappresentazione predefinita è 1e+21.
bobince

22

In alternativa puoi anche provare a convertire il dizionario in un elenco di formati [(k1, v1), (k2, v2)] mentre lo codifichi usando json e riconvertirlo in dizionario dopo averlo decodificato.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Credo che questo richiederà ancora un po 'di lavoro come avere una sorta di flag per identificare quali parametri devono essere convertiti in dizionario dopo averlo decodificato da json.


Buona soluzione per oggetti dict senza oggetti dict annidati!
Tom Yu

15

Rispondere alla tua domanda secondaria:

Può essere ottenuto utilizzando 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

Questa funzione funzionerà anche per i dict annidati e utilizza una comprensione dei dettami.

Se vuoi trasmettere anche i valori, usa:

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

Che verifica l'istanza dei valori e li lancia solo se sono oggetti stringhe (unicode per l'esattezza).

Entrambe le funzioni presuppongono che le chiavi (e i valori) siano numeri interi.

Grazie a:

Come usare if / else in una comprensione del dizionario?

Converte una chiave stringa in int in un dizionario


È stato fantastico. Nel mio caso il decapaggio non può essere utilizzato, quindi sto salvando il coraggio di un oggetto utilizzando JSON tramite la conversione in un byte_array in modo da poter utilizzare la compressione. Ho chiavi miste, quindi ho appena modificato il tuo esempio per ignorare un ValueError quando la chiave non è convertibile in un int
minillinim

11

Sono stato morso dallo stesso problema. Come altri hanno sottolineato, in JSON, le chiavi di mappatura devono essere stringhe. Puoi fare una delle due cose. Puoi usare una libreria JSON meno rigida, come demjson , che consente stringhe intere. Se nessun altro programma (o nessun altro in altre lingue) lo leggerà, allora dovresti stare bene. Oppure puoi usare un linguaggio di serializzazione diverso. Non suggerirei il sottaceto. È difficile da leggere e non è progettato per essere sicuro . Invece, suggerirei YAML, che è (quasi) un superset di JSON e consente chiavi intere. (Almeno PyYAML lo fa.)


2

Converti il ​​dizionario in stringa utilizzando str(dict)e quindi riconvertilo in dict in questo modo:

import ast
ast.literal_eval(string)

1

Ecco la mia soluzione! Ho usato object_hook, è utile quando hai annidatojson

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

Esiste un filtro solo per l'analisi della chiave json su int. Puoi anche usare il int(v) if v.lstrip('-').isdigit() else vfiltro per il valore json.


1

Ho creato un'estensione molto semplice della risposta di Murmel che penso funzionerà su un dizionario piuttosto arbitrario (incluso annidato) supponendo che possa essere scaricato da JSON in primo luogo. Tutte le chiavi che possono essere interpretate come numeri interi verranno convertite in int. Senza dubbio questo non è molto efficiente, ma funziona per i miei scopi di memorizzazione e caricamento da stringhe 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

Supponendo che tutte le chiavi nel dict originale siano numeri interi se possono essere convertiti in int, questo restituirà il dizionario originale dopo l'archiviazione come json. per esempio

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

Puoi scrivere il tuo json.dumpsda solo, ecco un esempio da djson : encoder.py . Puoi usarlo in questo modo:

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