Modul json Python, mengubah kunci kamus int menjadi string

132

Saya telah menemukan bahwa ketika berikut ini dijalankan, modul json python (termasuk sejak 2.6) mengubah kunci kamus int menjadi string.

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

Apakah ada cara mudah untuk mempertahankan kunci sebagai int, tanpa perlu mengurai string pada dump dan load. Saya percaya itu akan mungkin menggunakan kait yang disediakan oleh modul json, tetapi sekali lagi ini masih membutuhkan parsing. Apakah mungkin ada argumen yang saya abaikan? tepuk tangan, chaz

Sub-pertanyaan: Terima kasih atas jawabannya. Melihat json berfungsi seperti yang saya takuti, apakah ada cara mudah untuk menyampaikan jenis kunci dengan mungkin mengurai output dari dump? Juga saya harus mencatat kode yang melakukan dumping dan kode yang mengunduh objek json dari server dan memuatnya, keduanya ditulis oleh saya.

Charles Ritchie
sumber
23
kunci json harus berupa string
tonfa

Jawaban:

87

Ini adalah salah satu perbedaan halus di antara berbagai koleksi pemetaan yang dapat menggigit Anda. JSON memperlakukan kunci sebagai string; Python mendukung kunci yang berbeda hanya dengan tipe yang berbeda.

Dalam Python (dan tampaknya dalam Lua), kunci pemetaan (kamus atau tabel, masing-masing) adalah referensi objek. Dalam Python, mereka harus menjadi tipe yang tidak dapat diubah, atau mereka harus menjadi objek yang mengimplementasikan sebuah __hash__metode. (Dokumen Lua menyarankan agar ia secara otomatis menggunakan ID objek sebagai hash / kunci bahkan untuk objek yang bisa berubah dan bergantung pada string internal untuk memastikan bahwa string yang setara dipetakan ke objek yang sama).

Di Perl, Javascript, awk dan banyak bahasa lainnya, kunci untuk hash, array asosiatif atau apapun sebutannya untuk bahasa yang diberikan, adalah string (atau "skalar" di Perl). Dalam perl $foo{1}, $foo{1.0}, and $foo{"1"}semua referensi ke pemetaan yang sama di %foo--- kunci dievaluasi sebagai skalar!

JSON dimulai sebagai teknologi serialisasi Javascript. (JSON singkatan dari J ava S cript O bject N otation .) Secara alami JSON mengimplementasikan semantik untuk notasi pemetaannya yang konsisten dengan semantik pemetaannya.

Jika kedua ujung serialisasi Anda adalah Python maka Anda akan lebih baik menggunakan acar. Jika Anda benar-benar perlu mengubahnya kembali dari JSON menjadi objek Python asli, saya kira Anda memiliki beberapa pilihan. Pertama, Anda dapat mencoba ( try: ... except: ...) untuk mengonversi kunci apa pun menjadi angka jika pencarian kamus gagal. Alternatifnya, jika Anda menambahkan kode ke ujung yang lain (pembuat serial atau generator data JSON ini) maka Anda dapat membuatnya melakukan serialisasi JSON pada setiap nilai kunci --- menyediakannya sebagai daftar kunci. (Kemudian kode Python Anda pertama-tama akan mengulang daftar kunci, membuat instance / deserialisasi ke dalam objek Python asli ... dan kemudian menggunakannya untuk mengakses nilai dari pemetaan).

Jim Dennis
sumber
1
Terima kasih untuk itu. Sayangnya saya tidak bisa menggunakan Pickle, tapi ide Anda dengan daftarnya bagus. Akan menerapkannya sekarang, bersorak untuk idenya.
Charles Ritchie
1
(Kebetulan, dalam Python 1, 1L (bilangan bulat panjang), dan 1.0 dipetakan ke kunci yang sama; tetapi "1" (string) tidak dipetakan ke sama dengan 1 (bilangan bulat) atau 1.0 (mengambang) atau 1L (bilangan bulat panjang ).
Jim Dennis
5
Berhati-hatilah dengan rekomendasi penggunaan Pickle. Pickle dapat mengakibatkan eksekusi kode arbitrer, jadi jika sumber data yang Anda deserialisasi tidak dapat dipercaya secara inheren, Anda harus tetap menggunakan protokol serialisasi "aman" seperti JSON. Juga perlu diingat bahwa saat cakupan proyek berkembang, terkadang fungsi yang Anda harapkan hanya akan mendapatkan masukan tepercaya mulai mendapatkan masukan dari pengguna, dan pertimbangan keamanan tidak selalu ditinjau kembali.
AusIV
55

Tidak, tidak ada yang namanya kunci Angka dalam JavaScript. Semua properti objek diubah menjadi String.

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

Ini dapat menyebabkan beberapa perilaku yang tampak aneh:

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

Objek JavaScript bukanlah pemetaan yang tepat seperti yang Anda pahami dalam bahasa seperti Python, dan menggunakan kunci yang bukan String menghasilkan keanehan. Inilah sebabnya mengapa JSON selalu secara eksplisit menulis kunci sebagai string, meski tampaknya tidak perlu.

bobince
sumber
1
Mengapa tidak 999999999999999999999diubah menjadi '999999999999999999999'?
Piotr Dobrogost
4
@PiotrDobrogost JavaScript (seperti banyak bahasa) tidak dapat menyimpan angka besar secara sembarangan. The Numberjenis adalah IEEE 754 ganda nilai floating point: Anda mendapatkan 53 bit mantissa, sehingga Anda dapat menyimpan hingga 2⁵³ (9007199254740992) dengan akurasi integer; di luar itu bilangan bulat akan dibulatkan ke nilai lain (karenanya 9007199254740993 === 9007199254740992). 999999999999999999999 dibulatkan ke 1000000000000000000000, yang toStringrepresentasi defaultnya adalah 1e+21.
bobince
22

Alternatifnya, Anda juga dapat mencoba mengonversi kamus ke daftar format [(k1, v1), (k2, v2)] sambil menyandikannya menggunakan json, dan mengubahnya kembali ke kamus setelah mendekodekannya kembali.


>>>> import json
>>>> json.dumps(releases.items())
    '[[1, "foo-v0.1"]]'
>>>> releases = {1: "foo-v0.1"}
>>>> releases == dict(json.loads(json.dumps(releases.items())))
     True
Saya percaya ini akan membutuhkan lebih banyak pekerjaan seperti memiliki semacam bendera untuk mengidentifikasi apa semua parameter yang akan dikonversi ke kamus setelah mendekodekannya kembali dari json.

Ashish
sumber
Solusi bagus untuk objek dict tanpa objek dict bersarang!
Tom Yu
15

Menjawab subpertanyaan Anda:

Itu bisa dicapai dengan menggunakan 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

Fungsi ini juga akan bekerja untuk dicts bersarang dan menggunakan pemahaman dict.

Jika Anda ingin memasukkan nilai juga, gunakan:

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

Yang menguji contoh nilai dan melemparkannya hanya jika itu adalah objek string (unicode tepatnya).

Kedua fungsi tersebut mengasumsikan kunci (dan nilai) menjadi bilangan bulat.

Terimakasih untuk:

Bagaimana cara menggunakan if / else dalam pemahaman kamus?

Ubah kunci string menjadi int dalam Kamus

Murmel
sumber
Ini bagus. Dalam kasus saya pengawetan tidak dapat digunakan jadi saya menyimpan nyali objek menggunakan JSON melalui konversi ke byte_array sehingga saya dapat menggunakan kompresi. Saya punya kunci campuran, jadi saya baru saja memodifikasi contoh Anda untuk mengabaikan ValueError ketika kunci tidak dapat diubah menjadi int
minillinim
11

Saya telah digigit oleh masalah yang sama. Seperti yang ditunjukkan orang lain, di JSON, kunci pemetaan harus berupa string. Anda dapat melakukan salah satu dari dua hal. Anda dapat menggunakan pustaka JSON yang tidak terlalu ketat, seperti demjson , yang mengizinkan string integer. Jika tidak ada program lain (atau tidak ada program lain dalam bahasa lain) yang akan membacanya, Anda seharusnya baik-baik saja. Atau Anda dapat menggunakan bahasa serialisasi yang berbeda. Saya tidak menyarankan acar. Sulit untuk dibaca, dan tidak dirancang untuk aman . Sebagai gantinya, saya menyarankan YAML, yang (hampir) superset dari JSON, dan memungkinkan kunci integer. (Setidaknya PyYAML melakukannya.)

AFoglia
sumber
2

Ubah kamus menjadi string dengan menggunakan str(dict)lalu ubah kembali menjadi dict dengan melakukan ini:

import ast
ast.literal_eval(string)
Hzzkygcs
sumber
1

Inilah solusi saya! Saya dulu object_hook, ini berguna bila Anda telah bersarangjson

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

Ada filter hanya untuk mem-parsing kunci json ke int. Anda juga dapat menggunakan int(v) if v.lstrip('-').isdigit() else vfilter untuk nilai json.

GooDeeJaY
sumber
1

Saya membuat perpanjangan yang sangat sederhana dari jawaban Murmel yang menurut saya akan bekerja pada kamus yang cukup sewenang-wenang (termasuk bersarang) dengan asumsi itu dapat dibuang oleh JSON di tempat pertama. Setiap kunci yang dapat diartikan sebagai bilangan bulat akan dilemparkan ke int. Tidak diragukan lagi ini tidak terlalu efisien, tetapi berfungsi untuk tujuan saya menyimpan ke dan memuat dari string 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

Asumsikan bahwa semua kunci dalam dict asli adalah bilangan bulat jika dapat diubah menjadi int, maka ini akan mengembalikan kamus asli setelah disimpan sebagai json. misalnya

>>>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
Tim Anak
sumber
-1

Anda dapat menulis json.dumpssendiri, berikut adalah contoh dari djson : encoder.py . Anda bisa menggunakannya seperti ini:

assert dumps({1: "abc"}) == '{1: "abc"}'
sialan
sumber