首先希望大家明白

Python中的序列化首选从来就不是json, 使用json只是为了与其他应用通信. Python内部通讯, 对象的序列化请使用pickle, 就像你用C/C++, 基本是不会考虑json的, 仅在内部通讯时, 你该用ProtoBuf.

我遇到的一点问题

Python中免不了也要定义一些类, 自定义的类如果想要序列化, 就是一件麻烦事了. 对于下面这一个类, 如果我们想要格式化某个对象.

1
2
3
4
5
6
7
8
9
10
class Inner():
def __init__(self):
self.m_a = 'a'
self.m_b = 'b'

In [7]: i = Inner()

In [8]: json.dumps(i) # 然后就会遇到TypeError了, 因为它无法序列化
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)

这样我们就会考虑给这个Inner类增加一个函数, 变成类似下面这个样子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Inner():
""" 一个类 """
def __init__(self):
self.m_a = 'a'
self.m_b = 'b'

def to_json(self):
return json.dumps({
'm_a': self.m_a,
'm_b': self.m_b,
})


In [19]: i.to_json()
Out[19]: '{"m_a": "a", "m_b": "b"}'

然后, 你觉得世界就变得正常了, 一切的一切就满足了吗.


愿望是极好的, 但是… 正常的生活不是由一个类组成的. 上面的那个类名叫Inner, 那会不会有一个类叫Outer包含它呢, 好像很可能会有吧.

1
2
3
4
5
6
7
8
9
10
11

class Outer():
def __init__(self):
self.m_i = Inner()
self.m_c = 'c'

In [21]: o = Outer()

In [22]: json.dumps(o) # 同样的问题好像又发生了.
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)

于是你决定也向Outer中再加一个函数, 好像像下面这样也能达到效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Outer():
""" 一个类 """
def __init__(self):
self.m_i = Inner()
self.m_c = 'c'

def to_json(self):
return json.dumps({
'm_i': {
'm_a': self.m_i.m_a, # 这里可以重新定义一个函数
'm_b': self.m_i.m_b, # 为了方便理解, 我就直接写了
},
'm_c': self.m_c
})

In [26]: o.to_json()
Out[26]: '{"m_i": {"m_a": "a", "m_b": "b"}, "m_c": "c"}'

世界好像也不止有两个类吧.

现在, 我们遇到了一个问题, 再出现类似的情况怎么办, 我们要一层一层的去包裹. 而且是纯手动, 你们能想想这个过程有多么的特别吗, 而且每个对象需要调用 to_json这一类函数, 的确很难.

这个时候, 你该考虑Google, StackOverflow了, 也许有人曾经遇到过这样的问题, 我搜索的是python json dumps custome object这样的一串关键字.

How to make a class JSON serializable是第一个内容, 里面介绍了一些方法. 例如:

1
2
3
4
5
6
7
8
class CustomJsonEncoder(json.JSONEncoder):  # 定义一个继承自json.JSONEncoder的类
def default(self, obj):
if isinstance(obj, Outer):
return {'m_c': obj.m_c} # 为了简化, 我没有写inner,
return json.JSONEncoder.default(self, obj)

In [32]: json.dumps(o, cls=CustomJsonEncoder)
Out[32]: '{"m_c": "c"}'

借助这个方式, 我们可以更新原来的Outer与Inner类, 再与CustomJsonEncoder结合

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
26
27
28
29
30
31
32
33
34
class Inner():
""" 一个类 """
def __init__(self):
self.m_a = 'a'
self.m_b = 'b'

def to_json(self):
return {
'm_a': self.m_a,
'm_b': self.m_b,
}

class Outer():
""" 一个类 """
def __init__(self):
self.m_i = Inner()
self.m_c = 'c'

def to_json(self):
return {
'm_i': self.m_i,
'm_c': self.m_c
}

class CustomJsonEncoder(json.JSONEncoder): # 定义一个继承自json.JSONEncoder的类
def default(self, obj):
if getattr(obj, "to_json", None): # 如果有to_json这个方法, 就调用它
return obj.to_json()
return json.JSONEncoder.default(self, obj)

In [35]: o = Outer()

In [36]: json.dumps(o, cls=CustomJsonEncoder)
Out[36]: '{"m_i": {"m_a": "a", "m_b": "b"}, "m_c": "c"}'

你别说, 还是挺成功的. 这样一来, 自己的定义的类也可以实现to_json这个方法, 缺点就是每次json.dumps的时候需要加上后面这一串了cls=CustomJsonEncoder

能不能再优雅一点

json.dumps(o, cls=CustomJsonEncoder)这句话真的挺长的, 如果我们能简化 一点就好了, 还像原来那样json.dumps(o)多好.

我是比较幸运的, 又找到了How to make a class JSON serializable. 里面提供 了一种思路, 由于json.dumps时, 默认的JSONEncoder是可以被我们找到的. 既然如此, 将它替换也是可能的, 这是原文中的代码(原文中有其他方法, 也可以去读读看).

1
2
3
4
5
6
7
from json import JSONEncoder

def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder().default
JSONEncoder.default = _default

我在自己的项目中, 换用了另外一种形式, 我希望原始的default最先被调用, 而后才是 我们自定义的to_json, 另外对于项目中的Decimal对象, 原始的函数似乎也不能用, 我就只能单独放出来了.

1
2
3
4
5
6
7
8
9
10
11
12
13
def _default(self, obj):
""" 以补丁的形式重写了json.JSONEncoder.default函数
避免在每个json.dumps处修改
"""
if isinstance(obj, Decimal):
return float(obj)

try:
orig_default(self, obj)
except:
return getattr(obj.__class__, "to_json")(obj)

json.JSONEncoder.default = _default

** << Dive Into Python >> ** 在Python中没有常量, 如果你试图努力的话什么都可以改变. 这一点满足Python的核心原则之一: 坏的行为应该被克服而不是被取缔.

你们认为这个方法怎么样?

好处是, 你可以将cls=CustomJsonEncoder去掉了.

而随之而来的坏处就是, 你的程序修改了原始的JSONEncoder中的default, 你看我在我 上面的程序中, 尽最大努力让程序先调用原来的default, 即便这样, Decimal对象在 序列化时仍然出现了问题, 不得不单独提出来.

另外一个问题就是, 你每定义一个类, 就意味着你需要实现一个to_json方法, 这种隐式的约定, 永远是可能出现问题的地方.

所幸, 我们平时使用也应该使用的是pickle, 而不是json.

json应该只在这一种情况下使用: 你想要与其他语言的程序通讯时.