อินสแตนซ์ของคลาสเป็นอนุกรมกับ JSON


186

ฉันกำลังพยายามสร้างการแสดงสตริง JSON ของอินสแตนซ์ของชั้นเรียนและมีปัญหา สมมติว่าคลาสนั้นถูกสร้างขึ้นเช่นนี้:

class testclass:
    value1 = "a"
    value2 = "b"

การเรียก json.dumps ทำดังนี้:

t = testclass()
json.dumps(t)

มันล้มเหลวและบอกฉันว่า testclass ไม่ใช่ JSON ต่อเนื่องกันได้

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

ฉันได้ลองใช้โมดูลดอง:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

และให้ข้อมูลอินสแตนซ์ของคลาส แต่ไม่ใช่เนื้อหาที่ต่อเนื่องกันของอินสแตนซ์ของคลาส

b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

ผมทำอะไรผิดหรือเปล่า?



30
ใช้หนึ่งบรรทัดs = json.dumps(obj, default=lambda x: x.__dict__)เพื่อตัวแปรเช่นอันดับของวัตถุ ( self.value1, self.value2, ... ) มันเป็นวิธีที่ง่ายที่สุดและตรงไปตรงมาที่สุด มันจะทำให้เป็นอันดับโครงสร้างของวัตถุที่ซ้อนกัน defaultฟังก์ชั่นจะถูกเรียกเมื่อวัตถุใด ๆ ที่กำหนดจะไม่ serializable โดยตรง คุณสามารถดูคำตอบของฉันด้านล่าง ฉันพบคำตอบยอดนิยมที่ซับซ้อนเกินความจำเป็นซึ่งอาจเป็นจริงมานานแล้ว
codeman48

1
คุณtestclassไม่มี__init__()เมธอดดังนั้นอินสแตนซ์ทั้งหมดจะใช้แอตทริบิวต์ class สองคลาสเดียวกัน ( value1และvalue2) ที่กำหนดไว้ในคำสั่งคลาส คุณเข้าใจความแตกต่างระหว่างคลาสและอินสแตนซ์ของคลาสหรือไม่?
martineau

1
มีห้องสมุดไพ ธ อนสำหรับgithub.com/jsonpickle/jsonpickleนี้(แสดงความคิดเห็นเนื่องจากคำตอบอยู่ต่ำกว่าในเธรดและไม่สามารถเข้าถึงได้)
ความปรารถนาดี

คำตอบ:


238

ปัญหาพื้นฐานคือตัวเข้ารหัส JSON json.dumps()เท่านั้นที่รู้วิธีการเรียงลำดับชุดของประเภทวัตถุที่ จำกัด โดยค่าเริ่มต้นประเภทในตัวทั้งหมด รายการที่นี่: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

วิธีแก้ปัญหาที่ดีอย่างหนึ่งคือการทำให้คลาสของคุณสืบทอดจากJSONEncoderนั้นใช้JSONEncoder.default()ฟังก์ชันและทำให้ฟังก์ชันนั้นปล่อย JSON ที่ถูกต้องสำหรับคลาสของคุณ

วิธีง่ายๆที่จะโทรjson.dumps()ใน.__dict__สมาชิกของอินสแตนซ์ที่ นั่นคือ Python มาตรฐานdictและถ้าคลาสของคุณง่ายมันจะเป็น JSON ต่อเนื่องได้

class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

วิธีการดังกล่าวกล่าวถึงในการโพสต์บล็อกนี้:

    การทำให้เป็นอันดับของออบเจกต์ Python โดยพลการกับ JSON โดยใช้ __dict__


3
ฉันลองสิ่งนี้ ผลลัพธ์สุดท้ายของการเรียก json.dumps (t .__ dict__) เป็นเพียง {}
ferhan

6
นั่นเป็นเพราะคลาสของคุณไม่มี.__init__()ฟังก์ชันเมธอดดังนั้นอินสแตนซ์ของคลาสของคุณมีพจนานุกรมว่างเปล่า กล่าวอีกนัยหนึ่ง{}คือผลลัพธ์ที่ถูกต้องสำหรับโค้ดตัวอย่างของคุณ
steveha

3
ขอบคุณ นี่เป็นการหลอกลวง ฉันเพิ่มinitง่าย ๆโดยไม่มีพารามิเตอร์และตอนนี้เรียก json.dumps (t .__ dict__) ส่งคืนข้อมูลที่เหมาะสมในรูปแบบของ: {"value2": "345", "value1": "123"} ฉันได้เห็นโพสต์เช่น สิ่งนี้มาก่อนไม่แน่ใจว่าฉันต้องการ serializer แบบกำหนดเองสำหรับสมาชิกหรือไม่ถ้าต้องการinitไม่ได้กล่าวถึงอย่างชัดเจนหรือพลาด ขอบคุณ.
ferhan

3
งานนี้สำหรับชั้นเรียนเดียว แต่ไม่เกี่ยวข้องกับชั้นเรียนที่เกี่ยวข้อง objets
Nwawel A Iroume

2
@NwawelAIroume: จริง หากคุณมีวัตถุซึ่งเช่นมีหลายวัตถุในรายการข้อผิดพลาดยังคงเป็นis not JSON serializable
gies0r

57

มีวิธีหนึ่งที่ใช้งานได้ดีสำหรับฉันซึ่งคุณสามารถลองใช้ได้:

json.dumps()สามารถใช้พารามิเตอร์เริ่มต้นเป็นตัวเลือกซึ่งคุณสามารถระบุฟังก์ชั่น serializer ที่กำหนดเองสำหรับประเภทที่ไม่รู้จักซึ่งในกรณีของฉันดูเหมือน

def serialize(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

สอง ifs แรกนั้นใช้สำหรับวันที่และเวลาในการทำให้เป็นอนุกรมและจะมีการobj.__dict__ส่งคืนวัตถุอื่น ๆ

การโทรครั้งสุดท้ายดูเหมือนว่า:

json.dumps(myObj, default=serialize)

เป็นการดีโดยเฉพาะอย่างยิ่งเมื่อคุณทำการซีเรียลคอลเล็กชันเป็นลำดับและคุณไม่ต้องการโทรหา__dict__อย่างชัดเจนสำหรับทุกวัตถุ ที่นี่จะทำเพื่อคุณโดยอัตโนมัติ

จนถึงตอนนี้ทำงานได้ดีมากสำหรับฉันรอคอยความคิดของคุณ


NameError: name 'serialize' is not definedฉันได้รับ เคล็ดลับใด ๆ
Kyle Delaney

ดีมาก. เพียงสำหรับการเรียนที่มีช่อง:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastory

น่าทึ่งมากที่ภาษายอดนิยมดังกล่าวไม่มีใครซับเจสันได้วัตถุ ต้องเป็นเพราะมันไม่ได้พิมพ์แบบคงที่
TheRennen

49

คุณสามารถระบุdefaultพารามิเตอร์ที่มีชื่อในjson.dumps()ฟังก์ชั่น:

json.dumps(obj, default=lambda x: x.__dict__)

คำอธิบาย:

สร้างเอกสาร ( 2.7 , 3.6 ):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(ทำงานบน Python 2.7 และ Python 3.x)

หมายเหตุ: ในกรณีนี้คุณต้องการinstanceตัวแปรและไม่ใช่classตัวแปรตามตัวอย่างในคำถามที่พยายามจะทำ (ฉันถือว่าผู้ถามตั้งใจclass instanceจะเป็นวัตถุของคลาส)

ผมได้เรียนรู้นี้เป็นครั้งแรกจากคำตอบ @ phihag ของที่นี่ พบว่าเป็นวิธีที่ง่ายและสะอาดที่สุดในการทำงาน


6
สิ่งนี้ได้ผลสำหรับฉัน แต่เนื่องจากสมาชิก datetime.date ฉันเปลี่ยนมันเล็กน้อย:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins

@Dakota การทำงานที่ดีรอบ ๆ ; datetime.dateเป็นการใช้งาน C ดังนั้นจึงไม่มี__dict__คุณลักษณะ IMHO เพื่อประโยชน์ของความสม่ำเสมอdatetime.dateควรจะมีมัน ...
codeman48

22

ฉันเพิ่งทำ:

data=json.dumps(myobject.__dict__)

นี่ไม่ใช่คำตอบที่สมบูรณ์และถ้าคุณมีคลาสของวัตถุที่ซับซ้อนคุณจะไม่ได้ทุกอย่างแน่นอน อย่างไรก็ตามฉันใช้สิ่งนี้กับวัตถุง่าย ๆ ของฉัน

สิ่งหนึ่งที่ใช้งานได้ดีจริงๆคือคลาส "ตัวเลือก" ที่คุณได้รับจากโมดูล OptionParser ที่นี่มันเป็นไปตามคำขอของ JSON

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)

คุณอาจต้องการลบตนเองหากคุณไม่ได้ใช้สิ่งนี้ในชั้นเรียน
SpiRail

3
มันจะใช้ได้ดีตราบใดที่วัตถุไม่ได้ประกอบไปด้วยวัตถุอื่น
Haroldo_OK


5

JSON ไม่ได้มีความหมายจริงๆสำหรับซีเรียลไลซ์วัตถุ Python มันยอดเยี่ยมสำหรับการทำให้เป็นอันดับdictวัตถุ แต่pickleโมดูลเป็นสิ่งที่คุณควรใช้โดยทั่วไป ผลลัพธ์จากการpickleไม่สามารถอ่านได้ของมนุษย์จริงๆ แต่ควรคลายออกได้ดี หากคุณยืนยันการใช้ JSON คุณสามารถตรวจสอบjsonpickleโมดูลซึ่งเป็นวิธีไฮบริดที่น่าสนใจ

https://github.com/jsonpickle/jsonpickle


9
ปัญหาหลักที่ฉันเห็นกับ pickle คือมันเป็นรูปแบบเฉพาะของ Python ในขณะที่ JSON เป็นรูปแบบที่ไม่ขึ้นกับแพลตฟอร์ม JSON มีประโยชน์เป็นพิเศษหากคุณกำลังเขียนเว็บแอปพลิเคชันหรือแบ็กเอนด์สำหรับแอปพลิเคชันมือถือบางตัว ที่ได้รับการกล่าวขอบคุณสำหรับการชี้ไปที่ jsonpickle
Haroldo_OK

@Haroldo_OK jsonpickle ยังไม่ส่งออกไปยัง JSON แต่มนุษย์อ่านไม่ได้ใช่ไหม
Caelum

4

ต่อไปนี้เป็นสองฟังก์ชันง่าย ๆ สำหรับการทำให้เป็นอนุกรมของคลาสที่ไม่มีความซับซ้อน

ฉันใช้สิ่งนี้สำหรับสิ่งที่ประเภทการกำหนดค่าเพราะฉันสามารถเพิ่มสมาชิกใหม่ในชั้นเรียนโดยไม่มีการปรับรหัส

import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}

3

มีคำตอบที่ดีเกี่ยวกับวิธีเริ่มต้นทำสิ่งนี้ แต่มีบางสิ่งที่ควรทราบ:

  • เกิดอะไรขึ้นถ้าอินสแตนซ์ถูกซ้อนภายในโครงสร้างข้อมูลขนาดใหญ่
  • ถ้าหากต้องการชื่อชั้นด้วย
  • จะทำอย่างไรถ้าคุณต้องการยกเลิกการจัดลำดับอินสแตนซ์
  • ถ้าคุณใช้__slots__แทน__dict__ล่ะ
  • ถ้าคุณไม่ต้องการทำด้วยตัวเอง

json-tricksเป็นห้องสมุด (ที่ฉันทำและคนอื่นมีส่วนร่วม) ซึ่งสามารถทำสิ่งนี้ได้สักพัก ตัวอย่างเช่น:

class MyTestCls:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

cls_instance = MyTestCls(s='ub', dct={'7': 7})

json = dumps(cls_instance, indent=4)
instance = loads(json)

คุณจะได้รับอินสแตนซ์ของคุณกลับมา ที่นี่ json มีลักษณะเช่นนี้:

{
    "__instance_type__": [
        "json_tricks.test_class",
        "MyTestCls"
    ],
    "attributes": {
        "s": "ub",
        "dct": {
            "7": 7
        }
    }
}

หากคุณต้องการสร้างโซลูชันของคุณเองคุณอาจมองไปที่แหล่งที่มาjson-tricksเพื่อไม่ให้ลืมกรณีพิเศษบางอย่าง (เช่น__slots__)

มันยังทำประเภทอื่น ๆ เช่นอาร์เรย์ numpy, datetimes, จำนวนเชิงซ้อน; มันยังช่วยให้ความคิดเห็น


3

Python3.x

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

อย่างไรก็ตามนี่คือ CoDec

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

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

แก้ไข

จากการวิจัยเพิ่มเติมพบว่ามีวิธีพูดคุยทั่วไปโดยไม่ต้องใช้วิธีการลงทะเบียนSUPERCLASSโดยใช้metaclass

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s

2

ฉันเชื่อว่าแทนที่จะเป็นมรดกตามที่แนะนำในคำตอบที่ยอมรับได้ดีกว่าที่จะใช้ polymorphism มิฉะนั้นคุณจะต้องมีคำสั่งขนาดใหญ่ถ้าอื่นเพื่อปรับแต่งการเข้ารหัสของทุกวัตถุ นั่นหมายถึงสร้างตัวเข้ารหัสเริ่มต้นทั่วไปสำหรับ JSON เป็น:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

แล้วมีjsonEnc()ฟังก์ชั่นในแต่ละคลาสที่คุณต้องการทำให้เป็นอันดับ เช่น

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

จากนั้นคุณโทร json.dumps(classInstance,default=jsonDefEncoder)

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