通过multiprocessing.Queue传递类字典对象使其无法通过属性修改
其实我不太确定标题是否准确描述了问题。让我先给你看看代码。
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 个回答
我猜因为它是 dict
的一个子类,所以你的 AttrDict
被当作 dict
来处理了。特别是 __dict__
指向 self
的部分可能没有被保留下来。你可以通过一些特殊的方法来定制这个处理过程;可以参考这篇文章。
这其实不是一个多进程的问题,因为 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}