JSON编码/自定义对象的解码


总览

描述一个JSON编码/解码包含自定义类的实例(对象)的复杂结构的示例。

语言是Python 3.8.1。

对于Python,使用pickle序列化复杂数据更为容易,但是如果您想在Python之外进行读取或写入,或者希望在序列化后具有一定的可读性,通常会选择JSON

我认为除了这里描述的方法以外,还有其他方法,但是我在以下几点上喜欢这种方法。

  • 使用Python标准json模块。
  • 编码/解码逻辑可以为每个类别划分。
  • 定制和复杂的数据示例

    因为它用于解释,所以我不会使其变得如此复杂,但是我将尝试使用满足以下条件的数据。

  • 包含多个自定义类。
  • 自定义对象的属性还包含自定义对象。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Person:
        def __init__(self, name):
            self.name = name

    class Robot:
        def __init__(self, name, creator=None):
            self.name = name
            self.creator = creator

    alan = Person('Alan')
    beetle = Robot('Beetle', creator=alan)
    chappy = Robot('Chappy', creator=beetle)

    alan是人类,beetlechappy是机器人。
    在下面,我想列出一个机器人数据列表,并对列表进行编码/解码。

    1
    robots = [beetle, chappy]

    编码

    对象序列化为JSON字符串称为编码。
    此列表包含PersonRobot类的对象,因此您需要能够对其进行编码。

    简单编码

    首先,让我们对一个简单的Person类进行编码。

    确定编码规范

    要对自定义对象进行编码,必须决定如何对其进行编码(规范)。

    在这里,我们将类名称和属性内容作为名称-值对输出。
    在上述alan的情况下,假定JSON字符串如下。

    1
    {"class": "Person", "name": "Alan"}

    制作自定义编码器

    您可以通过在

    标准json.dumps函数中指定cls参数来使用自定义编码器。
    通过继承json.JSONEncoder并覆盖default方法来创建自定义编码器。
    由于对象包含在default方法的参数中,因此,如果以可以由json.JSONEncoder处理的形式(此处,dict仅包含str)返回它,就可以了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import json

    # Person オブジェクト用の、カスタムのエンコーダ
    class PersonEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, Person):
                return {'class': 'Person', 'name': obj.name}
            else:
                return super().default(obj)

    print(json.dumps(alan, cls=PersonEncoder))

    # 結果:
    {"class": "Person", "name": "Alan"}

    复合编码

    接下来,我们将创建类Robot的编码器,但这并不复杂。
    如我在"概述"中所写,编码逻辑分为多个类。

    1
    2
    3
    4
    5
    6
    7
    # Robot オブジェクト用の、カスタムのエンコーダ
    class RobotEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, Robot):
                return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
            else:
                return super().default(obj)

    PersonEncoder几乎相同。
    但是,它没有像以前的PersonEncoder那样进行。这是因为返回值中的creator的格式不能由json.JSONEncoder处理。
    我敢于以这种方式划分逻辑,并且在实际编码时,请将两个编码器一起使用。

    组合编码器

    要合并编码器,请使用多重继承创建一个新类。

    1
    2
    3
    4
    5
    6
    7
    8
    # 2つのエンコーダを継承した、新しいエンコーダ
    class XEncoder(PersonEncoder, RobotEncoder):
        pass

    print(json.dumps(robots, cls=XEncoder))

    # 結果:
    [{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]

    <详细信息>

    格式化结果时(单击以显示)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    print(json.dumps(robots, cls=XEncoder, indent=4))

    # 結果:
    [
        {
            "class": "Robot",
            "name": "Beetle",
            "creator": {
                "class": "Person",
                "name": "Alan"
            }
        },
        {
            "class": "Robot",
            "name": "Chappy",
            "creator": {
                "class": "Robot",
                "name": "Beetle",
                "creator": {
                    "class": "Person",
                    "name": "Alan"
                }
            }
        }
    ]

    此方法是因为您只能为

    json.dumps函数指定一个编码器类,但是即使对象类型的数量增加,也可以对其进行扩展。

    (补充)多重继承操作

    我将通过制作上面的XEncoder来简要解释为什么它能很好地工作。

    在Python类的多重继承中,按继承顺序引用属性。
    当调用XEncoderdefault方法时,首先转到继承的PersonEncoder default方法。

    如果objPerson对象,则

    PersonEncoder.default方法将返回dict本身,否则将调用超方法。

    在这种情况下,超级方法将是RobotEncoder.default而不是json.JSONEncoder.default
    这是Python的多重继承运动。

    如果

    RobotEncoder.default调用超方法,它将不再继承,因此处理将委派给原始超类json.JSONEncoder

    我还没有研究如何递归调用

    default方法,但是只要if语句做出类决定,即使继承顺序颠倒了,也可以得到相同的结果。

    解码

    编码相反,将JSON字符串反序列化为对象称为解码。
    您可以通过将object_hook参数传递给json.loads方法来向解码后的对象添加自定义处理。

    简单的object_hook示例

    首先,让我们看一个仅对Person类的对象进行编码并对其进行解码的示例。
    由于作为object_hook传递的函数接收到已解码的对象(dict等),因此当\\'class \\'的值为dict为\\'Person \\'时,编写处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # Person オブジェクト用のフック関数
    def person_hook(obj):
        if type(obj) == dict and obj.get('class') == 'Person':
            return Person(obj['name'])
        else:
            return obj

    # JSON 文字列にエンコードする
    alan_encoded = json.dumps(alan, cls=PersonEncoder)
    # JSON 文字列からデコードする
    alan_decoded = json.loads(alan_encoded, object_hook=person_hook)

    print(alan_decoded)
    print(alan_decoded.__class__.__name__, vars(alan_decoded))

    # 結果:
    <__main__.Person object at 0x0000027F67919AF0>
    Person {'name': 'Alan'}

    结合object_hook

    接下来,为Robot类创建一个object_hook,并创建一个将两者结合在一起的新函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Robot オブジェクト用のフック関数
    def robot_hook(obj):
        if type(obj) == dict and obj.get('class') == 'Robot':
            return Robot(obj['name'], creator=obj['creator'])
        else:
            return obj

    # 2つのフック関数を呼び出す、新しいフック関数
    def x_hook(obj):
        return person_hook(robot_hook(obj))

    组合功能

    x_hook也可以写成:它会更长一些,但是增加钩子的数量会更容易(应用钩子的顺序与上面的示例不同,但是没有问题)。

    1
    2
    3
    4
    5
    def x_hook(obj):
        hooks = [person_hook, robot_hook]
        for hook in hooks:
            obj = hook(obj)
        return obj

    使用它来编码/解码上面创建的机械手列表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # JSON 文字列にエンコードする
    robots_encoded = json.dumps(robots, cls=XEncoder)
    # JSON 文字列からデコードする
    robots_decoded = json.loads(robots_encoded, object_hook=x_hook)

    for robot in robots_decoded:
        print(robot)
        print(robot.__class__.__name__, vars(robot))

    # 結果:
    <__main__.Robot object at 0x0000027F67919A30>
    Robot {'name': 'Beetle', 'creator': <__main__.Person object at 0x0000027F67919B50>}
    <__main__.Robot object at 0x0000027F67919E50>
    Robot {'name': 'Chappy', 'creator': <__main__.Robot object at 0x0000027F679199D0>}

    编码一样(可能是因为它是从内部递归解码的),更改钩子的应用顺序不会更改结果。

    (补充)可以使用相同的方式自定义编码

    实际上,可以通过以相同方式提供功能来定制编码端。
    相反,如果尝试使解码侧成为解码器的子类,则将变得更加复杂。

    结合使用自定义编码逻辑时,如果只想编写具有多个继承的代码,则应选择创建子类的方法,如果要匹配解码端的样式,则应选择赋予功能。

    通过提供函数

    定制编码端的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def person_default(obj):
        if isinstance(obj, Person):
            return {'class': 'Person', 'name': obj.name}
        else:
            return obj

    def robot_default(obj):
        if isinstance(obj, Robot):
            return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
        else:
            return obj

    def x_default(obj):
        defaults = [person_default, robot_default]
        for default in defaults:
            obj = default(obj)
        return obj

    print(json.dumps(robots, default=x_default))

    # 結果:
    [{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]

    任务

    解码存在一些问题。
    在上面的示例中,第一个解码的机器人\\'Beetle \\'和\\'Beetle \\'是\\'Chappy \\'的creator,最初是同一对象。
    同样,那些\\'Beelte \\'中的creator \\'Alan \\'是同一对象。

    上述解码方法不能完全重现编码之前的情况,因为它并不意味着"重用已经创建的对象,因为它们具有相同的名称"。
    如果要这样做,可以为PersonRobot类创建一种机制,以便仅通过指定名称就可以从object_hook接收适当的对象。