自定义 JSON 编码器未被调用
我尝试在这个帖子中应用martineau的解决方案,结果发现我的情况稍微有点不同。不过,似乎有一些我不太明白的原因,导致在使用json.dump()方法时,自定义的编码器没有被调用。
from collections.abc import MutableMapping
import json
import numpy as np
class JSONSerializer(json.JSONEncoder):
def encode(self, obj):
# Convert dictionary keys that are tuples into strings.
if isinstance(obj, MutableMapping):
for key in list(obj.keys()):
if isinstance(key, tuple):
strkey = "%d:%d" % key
obj[strkey] = obj.pop(key)
return super().encode(obj)
class Agent(object):
def __init__(self, states, alpha=0.15, random_factor=0.2):
self.state_history = [((0, 0), 0)] # state, reward
self.alpha = alpha
self.random_factor = random_factor
# start the rewards table
self.G = {}
self.init_reward(states)
def init_reward(self, states):
for i, row in enumerate(states):
for j, col in enumerate(row):
self.G[(j,i)] = np.random.uniform(high=1.0, low=0.1)
def memorize(self):
with open("memory.json", "w") as w:
json.dump(self.G, w, cls=JSONSerializer)
if __name__ == "__main__":
robot = Agent(states=np.zeros((6, 6)), alpha=0.1, random_factor=0.25)
print(robot.G)
robot.memorize()
在测试字符串编码时,结果看起来是我预期的那样,但当调用json.dump时,自定义编码器似乎根本没有被调用。你知道这是为什么吗?
谢谢
1 个回答
这段内容主要讲的是Python中的json
包的一些实现细节,可能和你用的Python版本有关。无论如何,这些细节对你的实现很重要:
dump()
函数内部会调用你的编码器的iterencode()
方法(具体可以查看源代码的第169和176行 这里)。- 而
dumps()
函数内部则会调用encode()
(可以查看源代码的第231和238行)。
你可以通过调整encode()
并重写iterencode()
在你的JSONSerializer
中来验证这一点,像这样:
class JSONSerializer(json.JSONEncoder):
def encode(self, obj):
print("encode called")
... # Your previous code here
return super().encode(obj)
def iterencode(self, *args, **kwargs):
print("iterencode called")
return super().iterencode(*args, **kwargs)
… 你会发现你的测试代码只会打印出“iterencode called”,而不会打印“encode called”。
顺便提一下,你在问题中链接的另一个Stack Overflow问题似乎也有同样的问题,至少在使用比较新的Python版本时(我现在用的是3.11)——可以查看我对相关回答的评论。
我有两个解决方案:
- 要么在你的
Agent.memorize()
方法中使用dumps()
,例如这样:def memorize(self): with open("memory.json", "w") as w: w.write(json.dumps(self.G, cls=JSONSerializer))
- 或者把你自己的实现从
encode()
移动到iterencode()
,例如这样:
这个第二个解决方案的好处是它可以同时适用于class JSONSerializer(json.JSONEncoder): def iterencode(self, obj, *args, **kwargs): if isinstance(obj, MutableMapping): for key in list(obj.keys()): if isinstance(key, tuple): strkey = "%d:%d" % key obj[strkey] = obj.pop(key) yield from super().iterencode(obj, *args, **kwargs)
dump()
和dumps()
(见下面的说明)。
补充说明:dumps()
函数之后似乎也会调用iterencode()
,虽然我没有追踪源代码到具体的调用位置,但从我添加的打印信息来看,这确实发生了。这有以下几个影响:(1)在第一个提议的解决方案中,由于encode()
先被调用,我们可以在这里对数据进行所有调整,使其可以被JSON序列化,因此在后面调用iterencode()
时就不会再出错了。(2)在第二个提议的解决方案中,由于我们重新实现了iterencode()
,我们的数据在这个时候就会被处理成可以JSON序列化的格式。
更新:实际上还有两个解决方案。首先,感谢@AbdulAzizBarkat的评论:我们可以重写default()
方法。不过,我们需要确保传递给序列化的对象类型是常规编码器无法处理的,否则我们就永远无法到达default()
方法。在给定的代码中,我们可以直接将Agent
实例传递给dump()
或dumps()
,而不是它的字典字段G
。所以我们需要做两个调整:
- 调整
memorize()
以传递self
,而不是self.G
,例如这样:def memorize(self): with open("memory.json", "w") as w: json.dump(self, w, cls=JSONSerializer)
- 调整
JSONSerializer.default()
以处理Agent
实例,例如这样(我们不再需要encode()
和iterencode()
):class JSONSerializer(json.JSONEncoder): def default(self, obj): if isinstance(obj, Agent): new_obj = {} for key in obj.G.keys(): new_key = ("%d:%d" % key) if isinstance(key, tuple) else key new_obj[new_key] = obj.G[key] return new_obj # Return the adjusted dictionary return super().default(obj)
其次,可能最简单的解决方案是根本不使用自定义的JSONEncoder
,而是直接给json.dump()
提供一个可以JSON序列化的对象。我们可以通过将键的预处理移动到Agent.memorize()
来实现:
def memorize(self):
obj = {}
for key in self.G.keys():
new_key = ("%d:%d" % key) if isinstance(key, tuple) else key
obj[new_key] = self.G[key]
with open("memory.json", "w") as w:
json.dump(obj, w)
最后补充一下:在前两个解决方案中,原始字典的键会被修改,按照你问题中的代码。你可能不想这样,因为实际上你的实例不应该因为你导出它的副本而改变。所以你可能更希望创建一个新的字典,使用修改后的键和原始值。我在第三和第四个解决方案中考虑到了这一点。