Bagaimana cara membuat serialisasi JSON?

149

Saya memiliki Python setyang berisi objek __hash__dan __eq__metode untuk memastikan tidak ada duplikat yang disertakan dalam koleksi.

Saya perlu json menyandikan hasil ini set, tetapi melewati bahkan kosong setke json.dumpsmetode menimbulkan a TypeError.

  File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable

Saya tahu saya dapat membuat ekstensi ke json.JSONEncoderkelas yang memiliki defaultmetode khusus , tapi saya bahkan tidak yakin harus mulai dari mana untuk mengkonversi set. Haruskah saya membuat kamus dari setnilai - nilai dalam metode default, dan kemudian mengembalikan pengkodean itu? Idealnya, saya ingin membuat metode default dapat menangani semua tipe data yang disandikan oleh pembuat kode asli (Saya menggunakan Mongo sebagai sumber data sehingga tanggal tampaknya juga meningkatkan kesalahan ini)

Setiap petunjuk ke arah yang benar akan dihargai.

EDIT:

Terima kasih atas jawabannya! Mungkin saya seharusnya lebih tepat.

Saya menggunakan (dan meningkatkan) jawaban di sini untuk mengatasi keterbatasan setterjemahan, tetapi ada kunci internal yang menjadi masalah juga.

Objek dalam objek setkompleks yang diterjemahkan __dict__, tetapi mereka sendiri juga dapat berisi nilai untuk properti mereka yang bisa tidak memenuhi syarat untuk tipe dasar dalam json encoder.

Ada banyak jenis berbeda yang masuk ke dalam ini set, dan hash pada dasarnya menghitung id unik untuk entitas, tetapi dalam semangat sejati NoSQL tidak ada yang tahu persis apa isi objek anak.

Satu objek mungkin berisi nilai tanggal untuk starts, sedangkan yang lain mungkin memiliki beberapa skema lain yang tidak menyertakan kunci yang berisi objek "non-primitif".

Itulah sebabnya satu-satunya solusi yang dapat saya pikirkan adalah memperpanjang JSONEncoderuntuk mengganti defaultmetode untuk menghidupkan kasus yang berbeda - tapi saya tidak yakin bagaimana cara melakukannya dan dokumentasi ini ambigu. Dalam objek bersarang, apakah nilai yang dikembalikan dari defaultpergi dengan kunci, atau hanya menyertakan / membuang umum yang melihat seluruh objek? Bagaimana metode itu mengakomodasi nilai bersarang? Saya telah memeriksa pertanyaan-pertanyaan sebelumnya dan sepertinya tidak dapat menemukan pendekatan terbaik untuk pengkodean spesifik kasus (yang sayangnya sepertinya perlu saya lakukan di sini).

DeaconDesperado
sumber
3
kenapa begitu dict? Saya pikir Anda ingin membuat hanya listdari set dan kemudian meneruskannya ke encoder ... misalnya:encode(list(myset))
Constantinius
2
Alih-alih menggunakan JSON, Anda bisa menggunakan YAML (JSON pada dasarnya adalah bagian dari YAML).
Paolo Moretti
@ PaoloMoretti: Apakah itu membawa keuntungan? Saya tidak berpikir set adalah di antara tipe data YAML yang didukung secara universal, dan itu kurang didukung secara luas, terutama mengenai API.
@ PaoloMoretti Terima kasih atas masukan Anda, tetapi aplikasi frontend membutuhkan JSON sebagai jenis pengembalian dan persyaratan ini adalah untuk semua keperluan tetap.
DeaconDesperado
2
@delnan saya menyarankan YAML karena memiliki dukungan asli untuk set dan tanggal .
Paolo Moretti

Jawaban:

116

Notasi JSON hanya memiliki segelintir tipe data asli (objek, array, string, angka, boolean, dan null), jadi apa pun yang diserialisasi dalam JSON perlu dinyatakan sebagai salah satu dari tipe ini.

Seperti yang ditunjukkan dalam modul modul json , konversi ini dapat dilakukan secara otomatis oleh JSONEncoder dan JSONDecoder , tetapi kemudian Anda akan menyerahkan beberapa struktur lain yang mungkin Anda perlukan (jika Anda mengonversi set ke daftar, maka Anda kehilangan kemampuan untuk memulihkan secara teratur daftar; jika Anda mengonversi set ke kamus menggunakan dict.fromkeys(s)maka Anda kehilangan kemampuan untuk memulihkan kamus).

Solusi yang lebih canggih adalah membangun tipe kustom yang dapat hidup berdampingan dengan tipe JSON asli lainnya. Ini memungkinkan Anda menyimpan struktur bersarang yang mencakup daftar, set, dicts, desimal, objek datetime, dll .:

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
            return JSONEncoder.default(self, obj)
        return {'_python_object': pickle.dumps(obj)}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

Berikut adalah contoh sesi yang menunjukkan bahwa ia dapat menangani daftar, dikte, dan set:

>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]

>>> j = dumps(data, cls=PythonObjectEncoder)

>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]

Sebagai alternatif, mungkin berguna untuk menggunakan teknik serialisasi yang lebih umum seperti YAML , Twisted Jelly , atau modul acar Python . Masing-masing mendukung berbagai tipe data yang jauh lebih besar.

Raymond Hettinger
sumber
11
Ini adalah pertama kalinya saya mendengar bahwa YAML adalah tujuan yang lebih umum daripada JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML adalah superset dari JSON (hampir). Itu juga menambahkan tag untuk data biner, set, peta yang dipesan, dan cap waktu. Mendukung lebih banyak tipe data adalah apa yang saya maksudkan dengan "tujuan yang lebih umum". Anda tampaknya menggunakan frasa "tujuan umum" dalam arti yang berbeda.
Raymond Hettinger
4
Jangan lupa juga jsonpickle , yang dimaksudkan sebagai pustaka umum untuk mengambil objek Python ke JSON, seperti yang disarankan oleh jawaban ini.
Jason R. Coombs
4
Pada versi 1.2, YAML adalah superset ketat JSON. Semua JSON legal sekarang adalah YAML legal. yaml.org/spec/1.2/spec.html
steveha
2
contoh kode ini impor JSONDecodertetapi tidak menggunakannya
watsonic
115

Anda dapat membuat pembuat enkode khusus yang mengembalikan a listketika bertemu a set. Ini sebuah contoh:

>>> import json
>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'

Anda dapat mendeteksi tipe lain dengan cara ini juga. Jika Anda perlu mempertahankan bahwa daftar itu sebenarnya adalah set, Anda dapat menggunakan penyandian khusus. Sesuatu seperti return {'type':'set', 'list':list(obj)}mungkin bekerja.

Untuk ilustrasi tipe bertingkat, pertimbangkan untuk membuat serial ini:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

Ini memunculkan kesalahan berikut:

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

Ini menunjukkan bahwa pembuat enkode akan listmengembalikan hasilnya dan secara berulang memanggil serializer pada anak-anaknya. Untuk menambahkan serializer khusus untuk banyak jenis, Anda dapat melakukan ini:

>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       if isinstance(obj, Something):
...          return 'CustomSomethingRepresentation'
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'
jterrace
sumber
Terima kasih, saya mengedit pertanyaan untuk lebih menentukan bahwa ini adalah jenis hal yang saya butuhkan. Apa yang saya tidak bisa pahami adalah bagaimana metode ini akan menangani objek bersarang. Dalam contoh Anda, nilai balik adalah daftar untuk set, tetapi bagaimana jika objek yang diteruskan adalah set dengan tanggal (tipe data buruk lainnya) di dalamnya? Haruskah saya menelusuri kunci dalam metode default itu sendiri? Terima kasih banyak!
DeaconDesperado
1
Saya pikir modul JSON menangani objek bersarang untuk Anda. Setelah mendapat daftar kembali, itu akan beralih ke item daftar mencoba untuk menyandikan masing-masing. Jika salah satunya adalah tanggal, defaultfungsi tersebut akan dipanggil lagi, kali ini dengan objmenjadi objek tanggal, jadi Anda hanya perlu mengujinya dan mengembalikan representasi tanggal.
jterrace
Jadi metode default dapat dijalankan beberapa kali untuk satu objek yang diteruskan ke sana, karena itu juga akan melihat kunci individual setelah "didaftar"?
DeaconDesperado
Semacam itu, itu tidak akan dipanggil berkali-kali untuk objek yang sama , tetapi dapat berulang menjadi anak-anak. Lihat jawaban yang diperbarui.
jterrace
Bekerja persis seperti yang Anda gambarkan. Saya masih harus mencari tahu beberapa kesalahan, tetapi sebagian besar mungkin adalah hal-hal yang dapat di refactored. Terima kasih banyak atas bimbingan Anda!
DeaconDesperado
7

Saya mengadaptasi solusi Raymond Hettinger ke python 3.

Inilah yang telah berubah:

  • unicode lenyap
  • memperbarui panggilan ke orang tua defaultdengansuper()
  • gunakan base64untuk membuat cerita bersambung bytesjenis menjadi str(karena tampaknya bytesdalam python 3 tidak dapat dikonversi ke JSON)
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]
simlmx
sumber
4
Kode yang ditunjukkan di akhir jawaban ini untuk pertanyaan terkait menyelesaikan hal yang sama dengan [hanya] decoding dan pengkodean objek byte json.dumps()kembali ke / dari 'latin1', melewatkan base64hal - hal yang tidak perlu.
martineau
6

Hanya kamus, Daftar, dan tipe objek primitif (int, string, bool) yang tersedia di JSON.

Joseph Le Brech
sumber
5
"Jenis objek primitif" tidak masuk akal ketika berbicara tentang Python. "Objek bawaan" lebih masuk akal, tetapi terlalu luas di sini (sebagai permulaan: termasuk dict, daftar, dan juga set). (Terminologi JSON mungkin berbeda.)
string number array objek true false null
Joseph Le Brech
6

Anda tidak perlu membuat kelas pembuat enkode khusus untuk memasok defaultmetode - metode ini dapat dilewatkan sebagai argumen kata kunci:

import json

def serialize_sets(obj):
    if isinstance(obj, set):
        return list(obj)

    return obj

json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)

menghasilkan [1, 2, 3]semua versi Python yang didukung.

Antti Haapala
sumber
4

Jika Anda hanya perlu menyandikan set, bukan objek Python umum, dan ingin membuatnya mudah dibaca oleh manusia, versi sederhana jawaban Raymond Hettinger dapat digunakan:

import json
import collections

class JSONSetEncoder(json.JSONEncoder):
    """Use with json.dumps to allow Python sets to be encoded to JSON

    Example
    -------

    import json

    data = dict(aset=set([1,2,3]))

    encoded = json.dumps(data, cls=JSONSetEncoder)
    decoded = json.loads(encoded, object_hook=json_as_python_set)
    assert data == decoded     # Should assert successfully

    Any object that is matched by isinstance(obj, collections.Set) will
    be encoded, but the decoded value will always be a normal Python set.

    """

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        else:
            return json.JSONEncoder.default(self, obj)

def json_as_python_set(dct):
    """Decode json {'_set_object': [1,2,3]} to set([1,2,3])

    Example
    -------
    decoded = json.loads(encoded, object_hook=json_as_python_set)

    Also see :class:`JSONSetEncoder`

    """
    if '_set_object' in dct:
        return set(dct['_set_object'])
    return dct
NeilenMarais
sumber
1

Jika Anda hanya perlu dump cepat dan tidak ingin mengimplementasikan encoder kustom. Anda dapat menggunakan yang berikut ini:

json_string = json.dumps(data, iterable_as_array=True)

Ini akan mengubah semua set (dan iterables lainnya) menjadi array. Berhati-hatilah karena bidang-bidang itu akan tetap array ketika Anda mengurai json kembali. Jika Anda ingin mempertahankan jenisnya, Anda perlu menulis pembuat enkode khusus.

David Novák
sumber
7
Ketika saya mencoba ini saya mendapatkan: TypeError: __init __ () mendapat argumen kata kunci tak terduga 'iterable_as_array'
atm
Anda perlu menginstal simplejson
JerryBringer
impor simplejson sebagai json dan kemudian json_string = json.dumps (data, iterable_as_array = True) berfungsi dengan baik di Python 3.6
fraverta
1

Salah satu kekurangan dari solusi yang diterima adalah bahwa outputnya sangat spesifik python. Yaitu output mentah json tidak dapat diamati oleh manusia atau diambil oleh bahasa lain (misalnya javascript). contoh:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

Akan membuat Anda:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

Saya dapat mengusulkan solusi yang menurunkan set ke dikt yang berisi daftar di jalan keluar, dan kembali ke set ketika dimuat ke python menggunakan encoder yang sama, karena itu menjaga agnostisisme pengamatan dan bahasa:

from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        elif isinstance(obj, set):
            return {"__set__": list(obj)}
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '__set__' in dct:
        return set(dct['__set__'])
    elif '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])

Yang membuat Anda:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

Perhatikan bahwa membuat serialisasi kamus yang memiliki elemen dengan kunci "__set__"akan merusak mekanisme ini. Jadi __set__sekarang telah menjadi dictkunci yang dipesan . Jelas merasa bebas untuk menggunakan kunci lain yang lebih dalam dan dikaburkan.

sagisme
sumber