自定义 JSON 编码器未被调用

1 投票
1 回答
74 浏览
提问于 2025-04-12 13:09

我尝试在这个帖子中应用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 个回答

2

这段内容主要讲的是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)——可以查看我对相关回答的评论

我有两个解决方案:

  1. 要么在你的Agent.memorize()方法中使用dumps(),例如这样:
    def memorize(self):
        with open("memory.json", "w") as w:
            w.write(json.dumps(self.G, cls=JSONSerializer))
    
  2. 或者把你自己的实现从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。所以我们需要做两个调整:

  1. 调整memorize()以传递self,而不是self.G,例如这样:
    def memorize(self):
         with open("memory.json", "w") as w:
             json.dump(self, w, cls=JSONSerializer)
    
  2. 调整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)

最后补充一下:在前两个解决方案中,原始字典的键会被修改,按照你问题中的代码。你可能不想这样,因为实际上你的实例不应该因为你导出它的副本而改变。所以你可能更希望创建一个新的字典,使用修改后的键和原始值。我在第三和第四个解决方案中考虑到了这一点。

撰写回答