用于持久化对象的类?
我正在尝试写一个类,这个类代表一个只读的对象。这个对象不会被copy
模块真正复制,而且当它被序列化(也就是“打包”)以便在不同的进程之间传输时,每个进程最多只会保留一个副本。无论这个对象被当作“新”对象传递多少次,都不会有多个副本。请问有没有类似的东西?
3 个回答
你可以简单地使用一个字典,字典里的键和值可以是一样的。在接收方使用这个字典时,为了避免内存泄漏,可以使用一个弱引用字典(WeakKeyDictionary)。
我不知道有没有现成的这种功能。这里有个有趣的问题,需要明确的规格来说明在这种情况下应该怎么处理……:
- 进程A创建了一个对象,然后把它发送给B,B把这个对象解码,前面都没问题。
- A对这个对象做了修改X,同时B对它自己复制的对象做了修改Y。
- 现在任意一个进程把它的对象发送给另一个进程,另一个进程解码后:此时每个进程需要看到哪些对象的变化? 发送方是A还是B,这个有关系吗?也就是说,A是否“拥有”这个对象?或者还有其他的考虑吗?
如果你不在乎,比如说只有A拥有这个对象——只有A可以做修改并把对象发送给其他人,其他人不能也不会去修改——那么问题就简化为唯一标识对象——可以用一个GUID(全局唯一标识符)。这个类可以维护一个字典,把GUID映射到现有的实例(可能用弱引用字典,以避免不必要地保持实例活着,但这只是个旁支问题),并确保在合适的时候返回现有的实例。
但是如果需要更细致地同步变化,那就突然变成一个非常复杂的分布式计算问题,具体在什么情况下发生什么事情的规格需要非常仔细地确定(而且比我们大多数人都要小心——分布式编程非常棘手,除非严格遵循一些简单且经过验证的模式和习惯!)。
如果你能为我们明确这些规格,我可以给你一个大致的思路,告诉你我会怎么去满足这些要求。但我不会替你猜测规格;-)。
编辑:提问者已经澄清,似乎他只需要更好地理解如何控制 __new__
。这很简单:见 __getnewargs__
——你需要一个新式类,并使用协议2或更好的序列化(不过这些出于其他原因也是推荐的!),然后现有对象中的 __getnewargs__
可以简单地返回对象的GUID(这个GUID需要作为可选参数传给 __new__
)。所以 __new__
可以检查这个GUID是否在类的 memo
[[弱引用;-)]]字典中(如果在,就返回对应的对象值)——如果不在(或者没有传递GUID,意味着这不是解码,所以必须生成一个新的GUID),那么就创建一个真正的新对象(设置它的GUID;-) 并在类级别的 memo
中记录它)。
顺便说一下,要生成GUID,可以考虑使用标准库中的 uuid 模块。
我尝试实现了这个功能。@Alex Martelli 和其他人,请给我一些意见或改进建议。我觉得最后这个会放到GitHub上。
"""
todo: need to lock library to avoid thread trouble?
todo: need to raise an exception if we're getting pickled with
an old protocol?
todo: make it polite to other classes that use __new__. Therefore, should
probably work not only when there is only one item in the *args passed to new.
"""
import uuid
import weakref
library = weakref.WeakValueDictionary()
class UuidToken(object):
def __init__(self, uuid):
self.uuid = uuid
class PersistentReadOnlyObject(object):
def __new__(cls, *args, **kwargs):
if len(args)==1 and len(kwargs)==0 and isinstance(args[0], UuidToken):
received_uuid = args[0].uuid
else:
received_uuid = None
if received_uuid:
# This section is for when we are called at unpickling time
thing = library.pop(received_uuid, None)
if thing:
thing._PersistentReadOnlyObject__skip_setstate = True
return thing
else: # This object does not exist in our library yet; Let's add it
new_args = args[1:]
thing = super(PersistentReadOnlyObject, cls).__new__(cls,
*new_args,
**kwargs)
thing._PersistentReadOnlyObject__uuid = received_uuid
library[received_uuid] = thing
return thing
else:
# This section is for when we are called at normal creation time
thing = super(PersistentReadOnlyObject, cls).__new__(cls, *args,
**kwargs)
new_uuid = uuid.uuid4()
thing._PersistentReadOnlyObject__uuid = new_uuid
library[new_uuid] = thing
return thing
def __getstate__(self):
my_dict = dict(self.__dict__)
del my_dict["_PersistentReadOnlyObject__uuid"]
return my_dict
def __getnewargs__(self):
return (UuidToken(self._PersistentReadOnlyObject__uuid),)
def __setstate__(self, state):
if self.__dict__.pop("_PersistentReadOnlyObject__skip_setstate", None):
return
else:
self.__dict__.update(state)
def __deepcopy__(self, memo):
return self
def __copy__(self):
return self
# --------------------------------------------------------------
"""
From here on it's just testing stuff; will be moved to another file.
"""
def play_around(queue, thing):
import copy
queue.put((thing, copy.deepcopy(thing),))
class Booboo(PersistentReadOnlyObject):
def __init__(self):
self.number = random.random()
if __name__ == "__main__":
import multiprocessing
import random
import copy
def same(a, b):
return (a is b) and (a == b) and (id(a) == id(b)) and \
(a.number == b.number)
a = Booboo()
b = copy.copy(a)
c = copy.deepcopy(a)
assert same(a, b) and same(b, c)
my_queue = multiprocessing.Queue()
process = multiprocessing.Process(target = play_around,
args=(my_queue, a,))
process.start()
process.join()
things = my_queue.get()
for thing in things:
assert same(thing, a) and same(thing, b) and same(thing, c)
print("all cool!")