1.问题
json
是网络传输比较简单易用。python
中,json
和dict
对象可以相互转换,首先我们看下简单的dict对象转换。
student = {
'name': 'chaos',
'age': 18,
'school': {
'name': "tsinghua"
}
}
print(json.dumps(student))
输出为:
{"name": "chaos", "age": 18, "school": {"name": "tsinghua"}}
一般情况下,我们遇到的类型一般并不是dict
,而是class
。我们的问题是python
的class
如何转换成json
呢?虽然python
中class
与dict
比较像,转化成dict
是比较麻烦的过程。本文我们就看看如何把python
对象转换为dict
。
2.方案
首先我们直接使用@dataclass
进行类型的定义,然后再看普通的class
类型。我们先定义类型:
@dataclass
class School:
name: str
@dataclass
class Student:
name: str
age: int
school: School
class
有个属性__dict__
,该属性包换了class
的属性成员。最初的版本转换版本代码如下:
def obj2json(obj, atom_type: list = None, collect_type: list = None) -> str:
def _obj2dict(in_obj, dc: dict, _atom_type, _collect_type):
for key, value in in_obj.__dict__.items():
if value is None:
dc[key] = None
elif isinstance(value, _atom_type):
dc[key] = value
elif isinstance(value, _collect_type):
dc[key] = list()
for item in value:
sub_dc = dict()
_obj2dict(item, sub_dc, _atom_type, _collect_type)
dc[key].append(sub_dc)
else:
dc[key] = dict()
_obj2dict(value, dc[key], _atom_type, _collect_type)
ret = dict()
# 直接转化json节点的类型
if not atom_type:
_atom_type = (int, float, str, bool, bytes)
else:
_atom_type = tuple(set(atom_type + [int, float, str, bool, bytes]))
# 数组类型
if not collect_type:
_collect_type = (set, tuple, list)
else:
_collect_type = tuple(set(collect_type + [set, tuple, list]))
_obj2dict(obj, ret, _atom_type, _collect_type)
return json.dumps(ret)
-
_atom_type
类型参数,表示直接转换为json
节点。比如int
、str
。默认情况下,并没有包换复数类型Complex
。 -
_collect_type
类型参数,表示转换为json
的数组节点,比如list
、tuple
等等。
测试代码如下:
student = Student(name='chaos', age=18, school=School('tsinghua'))
ret = obj2json0(student)
print(ret)
输出:
{"name": "chaos", "age": 18, "school": {"name": "tsinghua"}}
这一版本其实看上去不错。不过,遇到下面这个情况时,转换就出了问题。
@dataclass
class School:
name: str
@dataclass
class Student:
name: str
age: int
school: School
contact: dict
def test_dataclass_002():
ret = json.dumps(student)
assert ret == {"name": "chaos", "age": 18, "school": {"name": "tsinghua"}}
def test_dataclass_003():
student = Student(name='chaos', age=18, school=School('tsinghua'), contact=dict(number='13988889999'))
ret = obj2json0(student)
print(ret)
运行时会抛出异常:
in_obj = {'number': '13988889999'}, dc = {}
_atom_type = (<class 'int'>, <class 'float'>, <class 'str'>, <class 'bool'>, <class 'bytes'>)
_collect_type = (<class 'set'>, <class 'tuple'>, <class 'list'>)
def _obj2dict(in_obj, dc: dict, _atom_type, _collect_type):
> for key, value in in_obj.__dict__.items():
E AttributeError: 'dict' object has no attribute '__dict__'
问题是对于内置类型dict
的对象,没有__dict__
属性。这也很好解决,先判定一下是否dict
类型,代码如下:
def obj2json(obj, atom_type: list = None, collect_type: list = None) -> str:
def _obj2dict(in_obj, dc: dict, _atom_type, _collect_type):
if isinstance(in_obj, dict):
dict_obj = in_obj
else:
dict_obj = in_obj.__dict__
for key, value in dict_obj.items():
if value is None:
dc[key] = None
elif isinstance(value, _atom_type):
dc[key] = value
elif isinstance(value, dict):
dc[key] = dict()
_obj2dict(value, dc[key], _atom_type, _collect_type)
elif isinstance(value, _collect_type):
dc[key] = list()
for item in value:
sub_dc = dict()
_obj2dict(item, sub_dc, _atom_type, _collect_type)
dc[key].append(sub_dc)
else:
dc[key] = dict()
_obj2dict(value, dc[key], _atom_type, _collect_type)
ret = dict()
if not atom_type:
_atom_type = (int, float, str, bool, bytes)
else:
_atom_type = tuple(set(atom_type + [int, float, str, bool, bytes]))
if not collect_type:
_collect_type = (set, tuple, list)
else:
_collect_type = tuple(set(collect_type + [set, tuple, list]))
_obj2dict(obj, ret, _atom_type, _collect_type)
return json.dumps(ret)
我们现在看看上述的转换,能否适用于普通的class
。代码如下:
class School:
def __init__(self, name):
self.name = name
class Student:
def __init__(self, name, age, contact, school):
self.name = name
self.age = age
self.school = school
self.contact = contact
def get_name(self):
return self.name
@classmethod
def get_class_name(cls):
return "Student"
student = Student(name='chaos', age=18, contact=dict(number='13999998888'), school=School('tsinghua'))
ret = obj2json(student)
print(ret)
输出如下:
{"name": "chaos", "age": 18, "school": {"name": "tsinghua"}, "contact": {"number": "13999998888"}}
看上去很好,新定义的对象函数或者类函数,都没有影响到结果输出。
3.讨论
到目前为止,转换函数看上去不错。不过,并不完美,此问题比看上去复杂。比如,对于双向链表这样的函数,就没有办法处理,会出现循环引用。比如下面这个代码:
class TreeNode:
def __init__(self, value, pre_node=None, next_node=None):
self.value = value
self.pre_node = pre_node
self.next_node = next_node
root = TreeNode(10)
next_root = TreeNode(11)
root.pre_node = next_root
root.next_node = next_root
next_root.pre_node = root
next_root.next_node = root
obj2json(root)
进一步思考的话,可能也会有解决办法,但是我们需要的是解决当前的问题,不一定需要解决所有情况下的问题。如果实现过于复杂的话,不妨到此为止,只要能解决当前的问题就行,将来遇到再说也不迟。