如何正确地将JSON转换为Python对象?

5 投票
3 回答
831 浏览
提问于 2025-04-30 11:43

我想把从网络服务获取的JSON数据解析成一个对象结构。因此,我正在实现一个json.JSONDecoder的子类,并添加一个object_hook方法。不过,我还没找到一个好的方法来选择适合给定数据的类。对于那些属性相同的类,似乎很难确定正确的类,因为这需要知道具体的键。我们来看一个例子:

我有以下几个类:

class Post:
    def __init__(self, title, user=None, group=None):
        self.title = title
        self.user = user
        self.group = group

class Group:
    def __init__(self, name):
        self.name = name

class User:
    def __init__(self, name):
        self.name = name

注意到GroupUser类有相同的属性。现在我的JSONDecoder看起来是这样的:

 class JSONDecoder(json.JSONDecoder):

    def __init__(self, encoding="UTF-8"):
        json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)

    def dict_to_object(self, d):

        if "posts" in d:
            return d["posts"]
        if "title" in d:
            if "user" in d:
               return Post(d["title"], user=d["user"])
            if "group" in d:
               return Post(d["title"], group=d["group"])
        if "name" in d:
            # How to decide if User(d["name"]) or Group(d["name")?
            return None
        return None

当它看到一个包含“name”这个键的字典时,它无法决定是创建一个Group对象还是一个User对象(所以我现在返回None)。

我想解析的JSON字符串如下:

s = """
{ "posts" : [ 
    {"title" : "Hello World", "user" : {"name" : "uli"}},
    {"title" : "Hello Group", "group" : {"name" : "Workgroup"}}
  ]
}
"""

这应该会生成一个Post对象的列表,每个对象都有一个标题和一个组或用户。

这个问题该怎么解决呢?在dict_to_object中使用一堆if语句真的是解决办法吗?(实际上代码看起来会更复杂,因为JSON结构嵌套得很深。)或者有没有其他的模式或库可以使用?(不过我更倾向于使用标准库。)

暂无标签

3 个回答

0

好的,下面是我最终解决这个问题的方法,而不是通过继承 json.JSONDecoder

class JSONDecoder:

    def decode_json(self, js):
        posts = []
        if "posts" in js:
            for p in js["posts"]:
                if "user" in p:
                    posts.append(Post(p["title"], user=self._decode_user(p["user"])))
                if "group" in p:
                    posts.append(Post(p["title"], group=self._decode_group(p["group"])))
        return posts

    def _decode_user(self, js):
        return User(js["name"])

    def _decode_group(self, js):
        return Group(js["name"])

你可以用 JSONDecoder().decode_json(json.loads(s)) 来调用它。顺便提一下,完整的代码可以在 Bitbucket上找到

0

一种方法是等到你手里有了UserGroup的标签后再去创建它们。也就是说,等到你在创建Post的时候再进行创建:

def dict_to_object(self, d):
    if "posts" in d:
        return d["posts"]
    if "title" in d:
        if "user" in d:
           d["user"] = User(d["user"]["name"])
        if "group" in d:
           d["group"] = Group(d["group"]["name"])
        return Post(d["title"],
                    d.get("user", None),
                    d.get("group", None))
    return d
0

在这种情况下,尤其是处理JSON解码时,最好的做法是在解码时先把数据放到一个通用的字典里,不要使用object_hook。等到所有数据都解码完后,再去创建具体的对象,这样你可以随意检查数据流和层级关系,比如哪个对象是父对象、子对象或兄弟对象。

使用类方法make_xyz函数,而不是构造函数

object_hook看起来很诱人,但实际上很少是你想要的。它虽然存在,但很多时候并不是正确的选择。只有在你能100%确定每个对象应该用哪个类的情况下,才可能是对的选择(即使这样,也要确保很容易判断,而不是在你的object-hook里写一个临时解析器),通常情况下,元素需要遵循某种顺序,JSON也不能有格式错误等等。

这里你遇到了一个通用的问题:在这个特定的情况下,一个构造函数看到{"name" : "xyz"}时,无法知道这是什么类型的JSON对象,只有看到"user"/"group" :的父对象才能知道。一个解决方案是把所有的类和构造函数重构为类方法make_group()make_user()。但这样做只是把第二次解码的过程合并到第一次解码中,没有特别的理由,这样会导致一个庞大且脆弱的object_hook函数。根据我的经验,这通常不是个好主意。

撰写回答