JSON kümeleri nasıl serileştirir?


154

Koleksiyona kopyaların eklenmediğinden emin olmak için setnesneler __hash__ve __eq__yöntemler içeren bir Python'um var .

Bu sonucu json kodlamam gerekiyor set, ancak yönteme bir boş bile setgeçirmek json.dumpsa 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

json.JSONEncoderÖzel bir defaultyöntemi olan sınıf için bir uzantı oluşturabileceğimi biliyorum , ancak set. setVarsayılan yöntemdeki değerlerden bir sözlük oluşturmalı ve ardından bunun üzerine kodlamayı döndürmeli miyim? İdeal olarak, varsayılan yöntemin, orijinal kodlayıcının tıkandığı tüm veri türlerini işleyebilmesini sağlamak istiyorum (Mongo'yu bir veri kaynağı olarak kullanıyorum, bu nedenle tarihler de bu hatayı artırıyor gibi görünüyor)

Doğru yönde herhangi bir ipucu takdir edilecektir.

DÜZENLE:

Cevap için teşekkürler! Belki de daha kesin olmalıydım.

Çevrilenin sınırlamalarını aşmak için buradaki cevapları kullandım (ve yükselttim) set, ancak bir sorun olan dahili anahtarlar da var.

İçindeki nesneler set, çevrilen karmaşık nesnelerdir __dict__, ancak kendileri de özellikleri için json kodlayıcısındaki temel türler için uygun olmayan değerler içerebilir.

Buna giren birçok farklı tür var setve karma temelde varlık için benzersiz bir kimlik hesaplıyor, ancak NoSQL'in gerçek ruhunda, alt nesnenin tam olarak ne içerdiğini söylemek mümkün değil.

Bir nesne için bir tarih değeri içerebilirken, bir startsbaşkası "ilkel olmayan" nesneler içeren anahtar içermeyen başka bir şemaya sahip olabilir.

Bu yüzden düşünebildiğim tek çözüm , farklı vakaları açmak JSONEncoderiçin defaultyöntemi değiştirmek için genişletmekti - ancak bunun nasıl yapılacağından emin değilim ve dokümantasyon belirsiz. Yuvalanmış nesnelerde, go'dan defaultanahtar tarafından döndürülen değer mi yoksa tüm nesneye bakan genel bir dahil etme / çıkarma işlemi mi? Bu yöntem iç içe geçmiş değerleri nasıl barındırır? Önceki soruları inceledim ve duruma özel kodlamaya en iyi yaklaşımı bulamıyorum (ki bu maalesef burada yapmam gereken şey gibi görünüyor).


3
neden dicts Sanırım listsetten sadece bir çıkış yapmak ve sonra onu kodlayıcıya geçirmek istiyorsunuz ... örneğin:encode(list(myset))
Constantinius

2
JSON kullanmak yerine YAML kullanabilirsiniz (JSON aslında YAML'nin bir alt kümesidir).
Paolo Moretti

@PaoloMoretti: Yine de herhangi bir avantaj sağlıyor mu? Kümelerin evrensel olarak desteklenen YAML veri türleri arasında olduğunu düşünmüyorum ve özellikle API'ler konusunda daha az yaygın olarak destekleniyor.

@PaoloMoretti Girişiniz için teşekkür ederiz, ancak uygulama ön ucu bir dönüş türü olarak JSON gerektirir ve bu gereksinim tüm amaçlar için sabittir.
DeaconDesperado

2
o ikisi için de bir yerel destek sunar çünkü YAML düşündüren oldu @delnan setleri ve tarihleri .
Paolo Moretti

Yanıtlar:


121

JSON gösteriminde yalnızca bir avuç yerel veri türü (nesneler, diziler, dizeler, sayılar, mantıksal değerler ve boş) vardır, bu nedenle JSON'da serileştirilen her şeyin bu türlerden biri olarak ifade edilmesi gerekir.

Json modülü belgelerinde gösterildiği gibi , bu dönüştürme bir JSONEncoder ve JSONDecoder tarafından otomatik olarak yapılabilir , ancak daha sonra ihtiyaç duyabileceğiniz bazı diğer yapılardan vazgeçmiş olursunuz (kümeleri bir listeye dönüştürürseniz, normal kurtarma yeteneğini kaybedersiniz. listeler; kullanarak setleri bir sözlüğe dönüştürürseniz, sözlükleri dict.fromkeys(s)kurtarma yeteneğini kaybedersiniz).

Daha karmaşık bir çözüm, diğer yerel JSON türleriyle bir arada bulunabilen özel bir tür oluşturmaktır. Bu, listeleri, kümeleri, diktleri, ondalık sayıları, tarih saat nesnelerini vb. İçeren iç içe geçmiş yapıları depolamanıza izin verir:

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

Listeleri, diktleri ve kümeleri işleyebileceğini gösteren örnek bir oturum:

>>> 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')]

Alternatif olarak, YAML , Twisted Jelly veya Python'un pikle modülü gibi daha genel amaçlı bir serileştirme tekniğini kullanmak faydalı olabilir . Bunların her biri, çok daha geniş bir veri türü aralığını destekler.


11
Bu, YAML'nin JSON'dan daha genel amaçlı olduğunu ilk duyduğum şey ... o_O
Karl Knechtel

14
@KarlKnechtel YAML, JSON'un bir üst kümesidir (neredeyse). Ayrıca ikili veriler, kümeler, sıralı haritalar ve zaman damgaları için etiketler ekler. Daha fazla veri türünü desteklemek, "daha genel amaç" ile kastettiğim şeydir. "Genel amaç" ifadesini farklı bir anlamda kullanıyor görünüyorsunuz.
Raymond Hettinger

5
Ayrıca unutmayın jsonpickle Bu cevap anlaşılacağı kadar, Python JSON itiraz dekapaj için genelleştirilmiş bir kütüphane olması amaçlanmıştır hangi.
Jason R. Coombs

5
Sürüm 1.2'den itibaren YAML, JSON'un katı bir üst kümesidir. Tüm yasal JSON artık yasal YAML'dir. yaml.org/spec/1.2/spec.html
steveha

2
örnek ithalatı bu kod JSONDecoderama kullanmaz
watsonic

118

Bir ile listkarşılaştığında bir döndüren özel bir kodlayıcı oluşturabilirsiniz set. İşte bir örnek:

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

Bu şekilde diğer türleri de tespit edebilirsiniz. Listenin aslında bir küme olduğunu korumanız gerekiyorsa, özel bir kodlama kullanabilirsiniz. Böyle bir şey return {'type':'set', 'list':list(obj)}işe yarayabilir.

İç içe yerleştirilmiş türleri göstermek için, bunu serileştirmeyi düşünün:

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

Bu, aşağıdaki hatayı ortaya çıkarır:

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

Bu, kodlayıcının listdöndürülen sonucu alacağını ve alt öğelerinde serileştiriciyi yinelemeli olarak çağıracağını gösterir. Birden çok tür için özel bir serileştirici eklemek için şunu yapabilirsiniz:

>>> 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"]'

Teşekkürler, ihtiyacım olan şeyin bu olduğunu daha iyi belirtmek için soruyu düzenledim. Anlayamadığım şey, bu yöntemin iç içe geçmiş nesneleri nasıl işleyeceği. Örneğinizde dönüş değeri küme listesidir, ancak ya iletilen nesne içinde tarihler (başka bir kötü veri türü) olan bir kümeyse? Varsayılan yöntemin kendisindeki anahtarları ayrıntılı olarak incelemeli miyim? Çok teşekkürler!
DeaconDesperado

1
JSON modülünün sizin için iç içe geçmiş nesneleri işlediğini düşünüyorum. Listeyi geri aldığında, her birini kodlamaya çalışan liste öğelerini yineleyecektir. Bunlardan biri bir tarih ise, defaultişlev bu kez objbir tarih nesnesi olarak yeniden çağrılacaktır, bu yüzden onu test etmeniz ve bir tarih temsili döndürmeniz yeterlidir.
jterrace

Öyleyse varsayılan yöntem, kendisine geçirilen herhangi bir nesne için birkaç kez çalışabilir, çünkü "listeye alındıktan" sonra tek tek anahtarlara da bakacaktır.
DeaconDesperado

Bir çeşit , aynı nesne için birden çok kez çağrılmaz , ancak çocuklarda tekrarlanabilir. Güncellenen yanıta bakın.
jterrace

Tam olarak tarif ettiğiniz gibi çalıştı. Hala bazı hataları çözmem gerekiyor, ancak çoğu muhtemelen yeniden düzenlenebilecek şeyler. Rehberliğiniz için çok teşekkürler!
DeaconDesperado

11

defaultYöntemi sağlamak için özel bir kodlayıcı sınıfı oluşturmanıza gerek yoktur - bir anahtar kelime argümanı olarak iletilebilir:

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)

[1, 2, 3]desteklenen tüm Python sürümleriyle sonuçlanır .


8

Raymond Hettinger'in çözümünü python 3'e uyarladım .

İşte değişen şey:

  • unicode kayboldu
  • ebeveynlerin çağrısı güncellenmiş defaultilesuper()
  • türü içine base64serileştirmek için kullanma (çünkü python 3'te JSON'a dönüştürülemiyor gibi görünüyor )bytesstrbytes
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')]

4
İlgili soruya verilen bu cevabın sonunda gösterilen kod , gerekli olmayan şeyleri atlayarak, bayt nesnesi json.dumps()geri dönüşlerinin [yalnızca] kodunu çözerek ve kodlayarak aynı şeyi başarır . 'latin1'base64
martineau

6

JSON'da yalnızca sözlükler, Listeler ve ilkel nesne türleri (int, string, bool) mevcuttur.


5
Python hakkında konuşurken "İlkel nesne türü" anlamsızdır. "Yerleşik nesne" daha mantıklıdır, ancak burada çok geniştir (yeni başlayanlar için: diktleri, listeleri ve ayrıca setleri içerir). (JSON terminolojisi farklı olabilir.)

dize numarası nesne dizisi true false null
Joseph Le Brech

4

Genel Python nesnelerini değil, yalnızca kümeleri kodlamanız gerekiyorsa ve bunları insanlar tarafından kolayca okunabilir durumda tutmak istiyorsanız, Raymond Hettinger'in yanıtının basitleştirilmiş bir sürümü kullanılabilir:

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

3

Hızlı döküme ihtiyacınız varsa ve özel kodlayıcı uygulamak istemiyorsanız. Aşağıdakileri kullanabilirsiniz:

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

Bu, tüm kümeleri (ve diğer yinelenenleri) dizilere dönüştürür. Sadece json'u geri ayrıştırdığınızda bu alanların diziler olarak kalacağına dikkat edin. Türleri korumak istiyorsanız, özel kodlayıcı yazmanız gerekir.


8
Bunu denediğimde şunu elde ediyorum: TypeError: __init __ () beklenmedik bir anahtar kelime argümanı 'iterable_as_array' aldı
atm

Simplejson
JerryBringer'ı

Simplejson'u json olarak içe aktarın ve ardından json_string = json.dumps (data, iterable_as_array = True) Python 3.6'da iyi çalışıyor
fraverta

1

Kabul edilen çözümün bir dezavantajı , çıktısının çok python'a özgü olmasıdır. Yani ham json çıktısı bir insan tarafından gözlemlenemez veya başka bir dil tarafından yüklenemez (örn. Javascript). misal:

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

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

Seni alacak:

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

Seti çıkışta bir liste içeren bir dikteye indirgeyen ve aynı kodlayıcıyı kullanarak python'a yüklendiğinde bir sete geri döndüren, böylece gözlemlenebilirliği ve dil bilinemezliğini koruyan bir çözüm önerebilirim:

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"])

Hangi sizi alır:

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

Unutmayın ki anahtarlı bir eleman içeren bir sözlüğü serileştirmek "__set__"bu mekanizmayı bozacaktır. Böylece __set__artık ayrılmış bir dictanahtar haline geldi . Açıkçası, daha derinden gizlenmiş başka bir anahtar kullanmaktan çekinmeyin.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.