JSON เป็นอนุกรมที่ตั้งได้อย่างไร?


149

ฉันมีงูหลามsetที่มีวัตถุด้วย__hash__และ__eq__วิธีการเพื่อให้แน่ใจว่าไม่มีการซ้ำซ้อนจะรวมอยู่ในคอลเลกชัน

ฉันจำเป็นต้องเข้ารหัส JSON ผลนี้setแต่ผ่านแม้ที่ว่างเปล่าsetกับวิธีการยกjson.dumpsTypeError

  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ระดับที่มีกำหนดเองdefaultวิธี setแต่ผมไม่แน่ใจว่าจะเริ่มต้นในการแปลงมากกว่า ฉันควรสร้างพจนานุกรมจากsetค่าภายในวิธีการเริ่มต้นแล้วส่งคืนการเข้ารหัสที่? เป็นการดีที่ฉันต้องการให้วิธีการเริ่มต้นสามารถจัดการกับประเภทข้อมูลทั้งหมดที่ตัวเข้ารหัสดั้งเดิมทำให้เกิด (ฉันใช้ Mongo เป็นแหล่งข้อมูลดังนั้นวันที่ดูเหมือนจะทำให้เกิดข้อผิดพลาดนี้ด้วย)

คำแนะนำใด ๆ ในทิศทางที่ถูกต้องจะได้รับการชื่นชม

แก้ไข:

ขอบคุณสำหรับคำตอบ! บางทีฉันควรจะแม่นยำมากขึ้น

ฉันใช้ประโยชน์ (และเพิ่มขึ้น) คำตอบที่นี่เพื่อแก้ไขข้อ จำกัด ของsetการแปล แต่มีคีย์ภายในที่เป็นปัญหาเช่นกัน

วัตถุในนั้นsetเป็นวัตถุที่ซับซ้อนที่แปลไป__dict__แต่พวกมันเองยังสามารถมีค่าสำหรับคุณสมบัติของพวกเขาที่อาจไม่เหมาะสมสำหรับประเภทพื้นฐานในตัวเข้ารหัส json

มีหลายประเภทที่เข้ามาในนี้setและแฮโดยทั่วไปจะคำนวณรหัสที่ไม่ซ้ำกันสำหรับเอนทิตี แต่ในจิตวิญญาณที่แท้จริงของ NoSQL ไม่มีการบอกสิ่งที่วัตถุลูกมี

วัตถุหนึ่งอาจมีค่าวันstartsที่ในขณะที่อีกวัตถุหนึ่งอาจมีสคีมาอื่น ๆ ที่ไม่มีคีย์ที่มีวัตถุ "ที่ไม่ใช่แบบดั้งเดิม"

นั่นคือเหตุผลที่ทางออกเดียวที่ฉันคิดได้คือขยายวิธีJSONEncoderการเปลี่ยนdefaultวิธีการเปิดกรณีที่แตกต่างกัน - แต่ฉันไม่แน่ใจว่าจะทำอย่างไรและเอกสารมีความคลุมเครือ ในวัตถุที่ซ้อนกันค่าที่ส่งคืนจากการdefaultไปตามคีย์หรือเป็นเพียงแค่การรวม / ทิ้งทั่วไปที่มองที่วัตถุทั้งหมดหรือไม่ วิธีนั้นรองรับค่าที่ซ้อนกันได้อย่างไร ฉันได้อ่านคำถามก่อนหน้านี้แล้วและไม่สามารถหาวิธีที่ดีที่สุดในการเข้ารหัสเฉพาะกรณี (ซึ่งน่าเสียดายที่ฉันต้องทำที่นี่)


3
ทำไมdictS? ฉันคิดว่าคุณต้องการสร้างlistออกมาจากชุดแล้วส่งไปยังตัวเข้ารหัส ... เช่น:encode(list(myset))
Constantinius

2
แทนที่จะใช้ JSON คุณสามารถใช้ YAML (JSON เป็นส่วนย่อยของ YAML)
เปาโล Moretti

@PaoloMoretti: มันนำมาซึ่งความได้เปรียบหรือไม่? ฉันไม่คิดว่าชุดข้อมูลเป็นประเภทข้อมูลที่ได้รับการสนับสนุนในระดับสากลของ YAML และมีการสนับสนุนอย่างกว้างขวางน้อยกว่าโดยเฉพาะเกี่ยวกับ API

@PaoloMoretti ขอบคุณสำหรับการป้อนข้อมูลของคุณ แต่ส่วนหน้าของแอปพลิเคชันต้องใช้ JSON เป็นประเภทส่งคืนและข้อกำหนดนี้ใช้สำหรับวัตถุประสงค์ทั้งหมดได้รับการแก้ไข
DeaconDesperado

2
@delnan ผมก็บอก YAML เพราะมีการสนับสนุนพื้นเมืองสำหรับทั้งชุดและวันที่
เปาโล Moretti

คำตอบ:


116

สัญกรณ์JSONมีเพียงหนึ่งประเภทข้อมูลดั้งเดิม (วัตถุ, อาร์เรย์, สตริง, ตัวเลข, บูลีนและโมฆะ) ดังนั้นสิ่งใดที่ต่อเนื่องกันใน JSON จะต้องมีการแสดงเป็นหนึ่งในประเภทเหล่านี้

ดังที่แสดงในเอกสาร json โมดูลการแปลงนี้สามารถทำได้โดยอัตโนมัติโดยJSONEncoderและJSONDecoderแต่จากนั้นคุณจะยอมแพ้โครงสร้างอื่น ๆ ที่คุณอาจต้องการ (ถ้าคุณแปลงชุดเป็นรายการแล้วคุณสูญเสียความสามารถในการกู้คืนปกติ รายการหากคุณแปลงชุดเป็นพจนานุกรมโดยใช้dict.fromkeys(s)แล้วคุณสูญเสียความสามารถในการกู้คืนพจนานุกรม)

โซลูชันที่ซับซ้อนยิ่งขึ้นคือการสร้างชนิดที่กำหนดเองที่สามารถอยู่ร่วมกับประเภท JSON ดั้งเดิมได้ สิ่งนี้ช่วยให้คุณสามารถเก็บโครงสร้างที่ซ้อนกันซึ่งรวมถึงรายการชุด, dicts, ทศนิยม, วัตถุวันที่และเวลา ฯลฯ :

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

นี่คือตัวอย่างเซสชันที่แสดงว่าสามารถจัดการรายการ dicts และ 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')]

ผลัดกันมันอาจจะมีประโยชน์ในการใช้เทคนิคเป็นอันดับอเนกประสงค์มากขึ้นเช่นYAML , Twisted วุ้นหรือ ธโมดูลดอง แต่ละประเภทจะสนับสนุนประเภทข้อมูลที่มากขึ้น


11
นี่เป็นครั้งแรกที่ฉันได้ยินมาว่า YAML มีจุดประสงค์ทั่วไปมากกว่า JSON ... o_O
Karl Knechtel

13
@KarlKnechtel YAML เป็นซูเปอร์เซ็ตของ JSON (เกือบมาก) นอกจากนี้ยังเพิ่มแท็กสำหรับข้อมูลไบนารี, ชุด, แผนที่สั่งซื้อและเวลาประทับ การสนับสนุนประเภทข้อมูลเพิ่มเติมคือสิ่งที่ฉันหมายถึงโดย "วัตถุประสงค์ทั่วไปมากขึ้น" คุณดูเหมือนจะใช้วลี "จุดประสงค์ทั่วไป" ในแง่ที่ต่างออกไป
Raymond Hettinger

4
อย่าลืมjsonpickleซึ่งมีวัตถุประสงค์เพื่อเป็นคลังข้อมูลทั่วไปสำหรับการเลือกวัตถุ Python ไปยัง JSON เช่นเดียวกับคำตอบนี้
Jason R. Coombs

4
ในฐานะของเวอร์ชัน 1.2, YAML เป็น superset ที่เข้มงวดของ JSON ตอนนี้ JSON ทางกฎหมายทั้งหมดเป็น YAML ที่ถูกกฎหมาย yaml.org/spec/1.2/spec.html
steveha

2
ตัวอย่างโค้ดนำเข้าJSONDecoderนี้ แต่ไม่ได้ใช้มัน
watsonic

115

คุณสามารถสร้างการเข้ารหัสที่กำหนดเองที่ส่งกลับเมื่อมันพบlist setนี่คือตัวอย่าง:

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

คุณสามารถตรวจจับชนิดอื่น ๆ ด้วยวิธีนี้เช่นกัน หากคุณต้องการรักษาว่ารายการนั้นเป็นชุดจริงคุณสามารถใช้การเข้ารหัสแบบกำหนดเองได้ สิ่งที่ต้องการreturn {'type':'set', 'list':list(obj)}อาจทำงานได้

หากต้องการแสดงประเภทที่ซ้อนกันให้พิจารณาการทำอนุกรมนี้

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

สิ่งนี้ทำให้เกิดข้อผิดพลาดดังต่อไปนี้:

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

สิ่งนี้บ่งชี้ว่าตัวเข้ารหัสจะนำlistผลลัพธ์ที่ได้คืนมาและโทรหาตัวเรียกลำดับซ้ำบนลูกของมัน ในการเพิ่ม serializer ที่กำหนดเองสำหรับหลายประเภทคุณสามารถทำได้:

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

ขอบคุณฉันแก้ไขคำถามเพื่อระบุว่านี่คือประเภทของสิ่งที่ฉันต้องการ สิ่งที่ฉันไม่เข้าใจคือวิธีการนี้จะจัดการกับวัตถุที่ซ้อนกัน ในตัวอย่างของคุณค่าส่งคืนคือรายการสำหรับชุด แต่ถ้าวัตถุที่ส่งผ่านเป็นชุดที่มีวันที่ (ประเภทข้อมูลที่ไม่ดีอื่น) อยู่ภายใน ฉันควรเจาะผ่านปุ่มต่าง ๆ ภายในวิธีการเริ่มต้นเองหรือไม่ ขอบคุณตัน!
DeaconDesperado

1
ฉันคิดว่าโมดูล JSON จัดการกับวัตถุที่ซ้อนกันสำหรับคุณ เมื่อได้รับรายการกลับมามันจะวนซ้ำรายการที่พยายามเข้ารหัสแต่ละรายการ หากหนึ่งในนั้นคือวันที่defaultฟังก์ชันจะถูกเรียกอีกครั้งคราวนี้ด้วยobjการเป็นวัตถุวันที่ดังนั้นคุณเพียงแค่ต้องทดสอบมันและส่งคืนการแสดงวันที่
jterrace

ดังนั้นวิธีการเริ่มต้นสามารถเรียกใช้หลาย ๆ ครั้งสำหรับวัตถุใด ๆ ที่ส่งไปยังมันเพราะมันจะดูที่ปุ่มแต่ละปุ่มเมื่อมัน "ฟัง"?
DeaconDesperado

เรียงจากมันจะไม่ถูกเรียกหลาย ๆ ครั้งสำหรับวัตถุเดียวกันแต่สามารถเรียกคืนไปยังเด็ก ๆ ได้ ดูคำตอบที่อัปเดต
jterrace

ทำงานตรงตามที่คุณอธิบาย ฉันยังคงต้องคิดหาข้อผิดพลาดบางอย่างออกมา แต่ส่วนใหญ่มันอาจเป็นสิ่งที่สามารถแก้ไขใหม่ได้ ขอบคุณสำหรับคำแนะนำของคุณ!
DeaconDesperado

7

ฉันปรับโซลูชันของ Raymond Hettinger เป็น python 3

นี่คือสิ่งที่มีการเปลี่ยนแปลง:

  • unicode หายไป
  • อัปเดตการโทรถึงผู้ปกครองdefaultด้วยsuper()
  • ใช้base64ในการทำให้เป็นอนุกรมbytesประเภทเป็นstr(เพราะดูเหมือนว่าbytesในหลาม 3 ไม่สามารถแปลงเป็น 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')]

4
รหัสที่แสดงในตอนท้ายของคำตอบของคำถามที่เกี่ยวข้องนี้จะทำสิ่งเดียวกันโดยการถอดรหัสเท่านั้นและการเข้ารหัสวัตถุไบต์json.dumps()กลับไปที่ / จาก'latin1'ข้ามbase64สิ่งที่ไม่จำเป็น
martineau

6

เฉพาะพจนานุกรมรายการและประเภทวัตถุดั้งเดิม (int, สตริง, บูล) มีให้ใน JSON


5
"ประเภทวัตถุดั้งเดิม" ไม่สมเหตุสมผลเมื่อพูดถึง Python "วัตถุในตัว" เหมาะสมกว่า แต่กว้างเกินไปที่นี่ (สำหรับผู้เริ่ม: รวม dicts รายการและชุด) (คำศัพท์ JSON อาจแตกต่างกันแม้ว่า)

จำนวนสตริงวัตถุแถวลำดับเป็นจริงโมฆะจริง
Joseph Le Brech

6

คุณไม่จำเป็นต้องสร้างคลาสของตัวเข้ารหัสแบบกำหนดเองเพื่อระบุdefaultวิธีการ - ซึ่งสามารถส่งผ่านเป็นอาร์กิวเมนต์ของคำหลักได้:

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]เวอร์ชัน Python ที่รองรับทั้งหมด


4

หากคุณต้องการเข้ารหัสชุดไม่ใช่วัตถุ Python ทั่วไปและต้องการให้ง่ายต่อการอ่านโดยมนุษย์คำตอบที่ง่ายของ Raymond Hettinger สามารถใช้:

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

1

หากคุณต้องการเพียงแค่การถ่ายโอนข้อมูลอย่างรวดเร็วและไม่ต้องการใช้โปรแกรมเปลี่ยนรหัสที่กำหนดเอง คุณสามารถใช้สิ่งต่อไปนี้:

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

นี้จะแปลงชุดทั้งหมด (และ iterables อื่น ๆ ) เป็นอาร์เรย์ เพียงแค่ระวังว่าฟิลด์เหล่านั้นจะคงอยู่อาร์เรย์เมื่อคุณแยก json กลับ หากคุณต้องการรักษาประเภทไว้คุณต้องเขียนตัวเข้ารหัสแบบกำหนดเอง


7
เมื่อฉันลองทำสิ่งนี้ฉันจะได้รับ: TypeError: __init __ () ได้รับการโต้แย้งคำหลักที่ไม่คาดคิด 'iterable_as_array'
atm

คุณต้องติดตั้ง simplejson
JerryBringer

นำเข้า simplejson เป็น json แล้ว json_string = json.dumps (ข้อมูล, iterable_as_array = True) ทำงานได้ดีใน Python 3.6
fraverta

1

หนึ่งในข้อบกพร่องของวิธีแก้ปัญหาที่ยอมรับคือเอาต์พุตนั้นมีความเฉพาะเจาะจงมาก เช่นมนุษย์ json เอาท์พุทดิบไม่สามารถสังเกตเห็นหรือโหลดด้วยภาษาอื่น (เช่นจาวาสคริปต์) ตัวอย่าง:

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

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

คุณจะได้รับ:

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

ฉันสามารถเสนอวิธีแก้ปัญหาซึ่งปรับลดชุดเป็น dict ที่มีรายการในทางออกและกลับไปที่ชุดเมื่อโหลดลงในหลามโดยใช้การเข้ารหัสเดียวกันดังนั้นการรักษาสังเกตและภาษาไม่เชื่อเรื่องพระเจ้า:

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

สิ่งที่ทำให้คุณได้รับ:

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

โปรดทราบว่าการเรียงลำดับพจนานุกรมที่มีองค์ประกอบพร้อมกับคีย์"__set__"จะทำให้กลไกนี้พัง ดังนั้น__set__ตอนนี้กลายเป็นdictรหัสที่สงวนไว้ เห็นได้ชัดว่ารู้สึกอิสระที่จะใช้คีย์อื่นที่ทำให้สับสนมากขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.