如何正确地将JSON转换为Python对象?
我想把从网络服务获取的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
注意到Group
和User
类有相同的属性。现在我的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 个回答
好的,下面是我最终解决这个问题的方法,而不是通过继承 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上找到。
一种方法是等到你手里有了User
和Group
的标签后再去创建它们。也就是说,等到你在创建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
在这种情况下,尤其是处理JSON解码时,最好的做法是在解码时先把数据放到一个通用的字典里,不要使用object_hook。等到所有数据都解码完后,再去创建具体的对象,这样你可以随意检查数据流和层级关系,比如哪个对象是父对象、子对象或兄弟对象。
使用类方法make_xyz函数,而不是构造函数
object_hook
看起来很诱人,但实际上很少是你想要的。它虽然存在,但很多时候并不是正确的选择。只有在你能100%确定每个对象应该用哪个类的情况下,才可能是对的选择(即使这样,也要确保很容易判断,而不是在你的object-hook里写一个临时解析器),通常情况下,元素需要遵循某种顺序,JSON也不能有格式错误等等。
这里你遇到了一个通用的问题:在这个特定的情况下,一个构造函数看到{"name" : "xyz"}
时,无法知道这是什么类型的JSON对象,只有看到"user"/"group" :
的父对象才能知道。一个解决方案是把所有的类和构造函数重构为类方法make_group()
和make_user()
。但这样做只是把第二次解码的过程合并到第一次解码中,没有特别的理由,这样会导致一个庞大且脆弱的object_hook函数。根据我的经验,这通常不是个好主意。