如何将树形类对象结构序列化为JSON文件格式?

10

给定以下代码示例,如何使用Python 3将这些类实例序列化为JSON?

class TreeNode():
    def __init__(self, name):
        self.name = name
        self.children = []

当我尝试执行json.dumps时,会出现以下错误:

TypeError: <TreeNode object at 0x7f6sf4276f60> is not JSON serializable

我发现,如果将默认的json.dumps设置为返回__dict__就可以成功序列化,但是这样做后执行json.loads就会出问题。

我能够找到很多基于基本字符串的自定义编码器/解码器示例,但没有涉及列表的情况,例如self.children。children列表将包含子节点及其其他节点的子节点。我需要一种方法来获取它们所有的信息。

2个回答

10

由于你正在处理一棵树形结构,使用嵌套字典是很自然的。下面的代码片段创建了一个 dict 的子类,并将其自身作为实例的底层 __dict__ —— 这是我在许多不同情境中遇到的一个有趣且有用的技巧:

     返回匿名类还是对象用来作为“结构体”更好? (stackoverflow)
     如何使用点号“.”访问字典成员? (stackoverflow)
     jsobject.py (PyDoc.net)
     制造像 JavaScript 对象一样的 Python 对象 (James Robert's 博客)
     AttrDict (ActiveState 配方)
     带有属性访问方式的字典 (ActiveState 配方)

事实上,这个技巧在 Python 中如此频繁,以至于我认为它是一个(不太知名的)Python习惯用法。

class TreeNode(dict):
    def __init__(self, name, children=None):
        super().__init__()
        self.__dict__ = self
        self.name = name
        self.children = list(children) if children is not None else []
这解决了序列化问题的一半,但当使用 json.loads() 读取生成的数据时,它将变成一个普通的字典对象,而不是 TreeNode 的实例。这是因为 JSONEncoder 可以自己编码字典(及其子类)。
解决这个问题的一种方法是,在 TreeNode 类中添加一种另类构造方法,可以调用该方法从 json.loads() 返回的嵌套字典中重建数据结构。
以下是我的意思:
    ...
    @staticmethod
    def from_dict(dict_):
        """ Recursively (re)construct TreeNode-based tree from dictionary. """
        node = TreeNode(dict_['name'], dict_['children'])
#        node.children = [TreeNode.from_dict(child) for child in node.children]
        node.children = list(map(TreeNode.from_dict, node.children))
        return node

if __name__ == '__main__':
    import json

    tree = TreeNode('Parent')
    tree.children.append(TreeNode('Child 1'))
    child2 = TreeNode('Child 2')
    tree.children.append(child2)
    child2.children.append(TreeNode('Grand Kid'))
    child2.children[0].children.append(TreeNode('Great Grand Kid'))

    json_str = json.dumps(tree, indent=2)
    print(json_str)
    print()
    pyobj = TreeNode.from_dict(json.loads(json_str))  # reconstitute
    print('pyobj class: {}'.format(pyobj.__class__.__name__))  # -> TreeNode
    print(json.dumps(pyobj, indent=2))

输出:

{
  "name": "Parent",
  "children": [
    {
      "name": "Child 1",
      "children": []
    },
    {
      "name": "Child 2",
      "children": [
        {
          "name": "Grand Kid",
          "children": [
            {
              "name": "Great Grand Kid",
              "children": []
            }
          ]
        }
      ]
    }
  ]
}

pyobj class: TreeNode
{
  "name": "Parent",
  "children": [
    {
      "name": "Child 1",
      "children": []
    },
    {
      "name": "Child 2",
      "children": [
        {
          "name": "Grand Kid",
          "children": [
            {
              "name": "Great Grand Kid",
              "children": []
            }
          ]
        }
      ]
    }
  ]
}

整洁,非常感谢。现在只需针对json.loads进行操作。当我们重新构建为Treenode时,最好的方法是什么? - Biff
Biff:你需要使用我在回复你的评论中添加的替代构造方法,(但我没有明确地在这里发表评论通知你——现在我只是为了其他读者的利益而这样做)。 - martineau

1
这里有一个备选答案,基本上是我的答案的 Python 3 版本,用于回答问题使用常规编码器使对象 JSON 可序列化,它可以 pickle 任何常规json编码器尚未处理的 Python 对象。
其中有几个不同之处。一是它不会对json模块进行猴子补丁,因为那不是解决方案的必要部分。另一个是,虽然这次的TreeNode类不是从dict类派生出来的,但它具有基本相同的功能。这是有意为之的,以防止库存的JSONEncoder对其进行编码,并导致在JSONEncoder子类中使用_default()方法。
除此之外,它是一个非常通用的方法,可以处理许多其他类型的 Python 对象,包括用户定义的类,而无需修改。
import base64
from collections import MutableMapping
import json
import pickle

class PythonObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        return {'_python_object': 
                base64.b64encode(pickle.dumps(obj)).decode('utf-8') }

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

# based on AttrDict -- https://code.activestate.com/recipes/576972-attrdict
class TreeNode(MutableMapping):
    """ dict-like object whose contents can be accessed as attributes. """
    def __init__(self, name, children=None):
        self.name = name
        self.children = list(children) if children is not None else []
    def __getitem__(self, key):
        return self.__getattribute__(key)
    def __setitem__(self, key, val):
        self.__setattr__(key, val)
    def __delitem__(self, key):
        self.__delattr__(key)
    def __iter__(self):
        return iter(self.__dict__)
    def __len__(self):
        return len(self.__dict__)

tree = TreeNode('Parent')
tree.children.append(TreeNode('Child 1'))
child2 = TreeNode('Child 2')
tree.children.append(child2)
child2.children.append(TreeNode('Grand Kid'))
child2.children[0].children.append(TreeNode('Great Grand Kid'))

json_str = json.dumps(tree, cls=PythonObjectEncoder, indent=4)
print('json_str:', json_str)
pyobj = json.loads(json_str, object_hook=as_python_object)
print(type(pyobj))

输出:

json_str: {
    "_python_object": "gANjX19tYWluX18KVHJlZU5vZGUKcQApgXEBfXECKFgIAAAAY2hp"
                      "bGRyZW5xA11xBChoACmBcQV9cQYoaANdcQdYBAAAAG5hbWVxCFgH"
                      "AAAAQ2hpbGQgMXEJdWJoACmBcQp9cQsoaANdcQxoACmBcQ19cQ4o"
                      "aANdcQ9oACmBcRB9cREoaANdcRJoCFgPAAAAR3JlYXQgR3JhbmQg"
                      "S2lkcRN1YmFoCFgJAAAAR3JhbmQgS2lkcRR1YmFoCFgHAAAAQ2hp"
                      "bGQgMnEVdWJlaAhYBgAAAFBhcmVudHEWdWIu"
}
<class '__main__.TreeNode'>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接