概述和背景
本文简单聊聊 python 的 json 序列化和反序列化,以及回调函数的设计
如果我们有一个配置文件,增加了一个配置项,我们希望这个配置项如果和历史的键相同能够合并它的值,应对如何使用 json 库来实现?
json 序列化和反序列化
讲一个内存对象按照约定的格式编码成二进制数据或某种可读的文件格式,叫做序列化,这个反过来的过程,则是反序列化
Python的 json 库提供了对于网络传输广泛使用的 JSON格式的序列化和反序列化。
例如
将文本字符串 s = " "{"key1": value1, "key2": value2}" 反序列化
import json
s = '{\"key1\": value1, \"key2\": value2}'
d = json.loads(s)
assert isinstance(d, dict) is True
序列化对应的 API 是dump
例子
s = '{"key1": 1, "key2":2, "key3": 3, "key1": 100}'
输出:
{'key1': 1, 'key2': 2, 'key3': 3}
关键字被key1 = 1 这一项被 key1 = 100 覆盖了。
问题
我们不希望是 json.loads() 的默认行为,该当如何
- 比如我们希望反序列化时能够找出重复的键值并将它们的值合并起来
- 或者,我们希望的是反序列化时,loads函数对于相同的键值始终保留第一次出现的值
我们不需要重新写一个 my_loads 这种新的函数来定义这些行为,事实上, 库作者早就预想到了这些奇奇怪怪的需要,仔细审视一下 loads/load 函数(这两个函数是类似的,load 的obj 是一个文件描述符,用于从一个磁盘文件中反序列化生成对象) 的参数设计
利用 hook 函数解决问题 1
定义一个 hook 函数
def hook_func(object):
result = {}
for k, v in object:
if k in result:
if isinstance(result[k], list):
result[k].append(v)
else:
result[k] = [result[k], v]
else:
result[k] = v
return result
重新反序列化 json 串
d = json.loads(s, object_pairs_hook=hook_func)
print(d)
输出
{'key1': [120, 100], 'key2': 2, 'key3': 3}
对于问题 2 ,保留最初的值的行为同样可以在hook 函数中定义——当发现重复的键我们略过就可以?
我们可能会问,这个 hook 函数对于深层嵌套字典会不会有异常行为?
搞清楚这点我们可以做两件事
-
测试一下
-
审查一下其内部工作机制
-
尝试解析 一下 '{"key1": 120, "key2":2, "key3": 3, "key1": 100, "key4": {"key1": 1}}' ,结果不会把 key4里面的key1 和外层 key1 当成相同的键
-
原因是 object_pairs_hook 和 object_hook 的调用都是逐层递归嵌套的,
'{"key1": 120, "key2":2, "key3": 3, "key1": 100, "key4": {"key1": 1}}' 首先按照深度优先的次序分解成有序对
[('key1', 1)] 解析成一个dict 对象之后 ,逐渐到外层形成的有序对
[('key1', 120), ('key2', 2), ('key3', 3), ('key1', 100), ('key4', {'key1': 1})]
由于这个缘故,写 hook 函数的时候需要注意,如果你希望精确地处理到每一个 json 串的内层,返回的东西,不能过于粗野(如不是列表或字典,会停止深入),否则内部无法逐层深入嵌套
...
doc 简译
``object_hook`` is an optional function that will be called with the
result of any object literal decode (a ``dict``). The return value of
``object_hook`` will be used instead of the ``dict``. This feature
can be used to implement custom decoders (e.g. JSON-RPC class hinting).
``object_pairs_hook`` is an optional function that will be called with the
result of any object literal decoded with an ordered list of pairs. The
return value of ``object_pairs_hook`` will be used instead of the ``dict``.
This feature can be used to implement custom decoders. If ``object_hook``
is also defined, the ``object_pairs_hook`` takes priority.
*object_hook* 是一个可选的函数,它会被调用于每一个解码出的对象字面量(即一个 `dict`)。*object_hook* 的返回值会取代原本的 [`dict`],这一特性能够被用于实现自定义解码器(如 [JSON-RPC](http://www.jsonrpc.org/) 的类型提示)。
*object_pairs_hook* 是一个可选的函数,它会被调用于每一个有序列表对解码出的对象字面量。 *object_pairs_hook* 的返回值将会取代原本的 dict 。这一特性能够被用于实现自定义解码器。如果 *object_hook* 也被定义, *object_pairs_hook* 优先。
*object_pairs_hook* 3.1 版本开始支持。
文档较难理解的部分:
object_hook, object_pairs_hook 都可以用作自定义解码行为,都会替换原本要返回的 dict ,它们的区别在于 object_hook 是字典对象的调用,意即传给 object_hook 的是一个字典,实际上已经被解码出来了,只是做一个键值替换;
而 object_pairs_hook 不是,运行到这个地方的时候,解码器得到的数据是一组类似于 [(a1, b2), (a2, b2), ... , (an, bn)] 的有序对,我们在 object_pairs_hook 函数里操作这个有序对。
``parse_float``, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
``parse_int``, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
``parse_constant``, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN.
This can be used to raise an exception if invalid JSON numbers
are encountered.
*parse_float, parse_int parse_constant * 作用类似,指定以后都是可以重写默认行为
以上看 object_hook 和 object_pair_hook 的区别我们就能明白为什么合并同键值对需要写的是 object_pair_hook 函数
python 文档中给了另一个例子
def as_complex(object):
if "__complex__" in object:
return complex(object['real'], object['imag'])
return object
complex_s = '{"__complex__": true, "real": 1, "imag": 2}'
print(json.loads(complex_s, object_hook=as_complex))
输出
(1+2j)
hook 回调函数的妙处
函数设计启示:
- 尽可能考虑各种使用场景,并抽象最基本的行为,然后通过默认函数或装饰器保留一些变化
- 对于并非绝对的逻辑,通过 hook 函数留出一些自定义行为的可能性,不要把 API 写死
我们用 json loads完成我们特定的需要的开发工作时,是不是情不自禁地感叹库作者的高明和绝妙,他并没有迫使我们重新去定义loads 的行为,否则就是库作者设计的一种失败
网友评论