通过multiprocessing.Queue传递类字典对象使其无法通过属性修改

0 投票
2 回答
1827 浏览
提问于 2025-04-17 22:45

其实我不太确定标题是否准确描述了问题。让我先给你看看代码。

import os
from multiprocessing import JoinableQueue

# A dict-like class, but is able to be accessed by attributes.
# example: d = AttrDict({'a': 1, 'b': 2})
# d.a is equivalent to d['a']
class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self


queue = JoinableQueue()
pid = os.fork()

if pid == 0:
    d = AttrDict({'a': 1, 'b': 2})
    queue.put(d)
    queue.join()
    os._exit(0)
else:
    d = queue.get()
    queue.task_done()
    #d = AttrDict(d.items())  #(1)
    d.a = 3                   #(2)
    #d['a'] = 3               #(3)
    print d

上面的代码输出了 {'a': 1, 'b': 2},这意味着(2)没有起到任何作用。

如果我把(2)改成(3),或者启用(1),那么输出就是 {'a': 3, 'b': 2},这就是我预期的结果。

看起来在通过队列传递 d 的时候发生了一些事情。

我是在 Python 2.7 中测试的。


解决方案:

正如 @kindall 和 @Blckknght 指出的那样,问题的原因是 d 被当作字典处理,当通过 queue.get() 解包时,self.__dict__ = self 这个魔法没有设置。通过 print d.__dict__print d 可以看到这个区别。

为了恢复这个魔法,我在 AttrDict 中添加了一个方法 __setstate__

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

    def __setstate__(self, state):
        self.__dict__ = state

现在代码按预期工作了。

2 个回答

1

我猜因为它是 dict 的一个子类,所以你的 AttrDict 被当作 dict 来处理了。特别是 __dict__ 指向 self 的部分可能没有被保留下来。你可以通过一些特殊的方法来定制这个处理过程;可以参考这篇文章

1

这其实不是一个多进程的问题,因为 mutlprocessing.Queue 使用 pickle 来处理你通过它发送的对象。问题在于 pickle 没有正确保留你在设置 self.__dict__ = self 时所获得的“魔法”行为。

如果你检查一下在子进程中得到的对象,你会发现它的 __dict__ 只是一个普通的字典,里面的内容和对象本身是一样的。当你在对象上设置一个新的属性时,它的 __dict__ 会更新,但继承的字典 self 不会。让我来解释一下:

>>> d = AttrDict({"a":1, "b":2})
>>> d2 = pickle.loads(pickle.dumps(d, -1))
>>> d2
{'a': 1, 'b': 2}
>>> d2.b = 3
>>> d2
{'a': 1, 'b': 2}
>>> d2.__dict__
{'a': 1, 'b': 3}

虽然你可以深入研究 pickle 的工作原理,让你的序列化再次正常工作,但我觉得一个更简单的方法是通过让你的类重写 __getattr____setattr____delattr__ 方法,来依赖于更简单的行为:

class AttrDict(dict):
    __slots__ = () # we don't need a __dict__

    def __getattr__(self, name): # wrapper around dict.__setitem__, with an exception fix
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name) from None # raise the right type of exception

    def __delattr__(self, name): # wrapper around dict.__delitem__
        try:
            del self[name]
        except KeyError:
            raise AttributeError(name) from None # change exception type here too

    __setattr__ = dict.__setitem__ # no special exception rewriting needed here

这个类的实例将像你自己的那样工作,但它们可以成功地被序列化和反序列化:

>>> d = AttrDict({"a":1, "b":2})
>>> d2 = pickle.loads(pickle.dumps(d, -1)) # serialize and unserialize
>>> d2
{'a': 1, 'b': 2}
>>> d2.b=3
>>> d2
{'a': 1, 'b': 3}

撰写回答