用于持久化对象的类?

5 投票
3 回答
3838 浏览
提问于 2025-04-15 14:11

我正在尝试写一个类,这个类代表一个只读的对象。这个对象不会被copy模块真正复制,而且当它被序列化(也就是“打包”)以便在不同的进程之间传输时,每个进程最多只会保留一个副本。无论这个对象被当作“新”对象传递多少次,都不会有多个副本。请问有没有类似的东西?

3 个回答

0

你可以简单地使用一个字典,字典里的键和值可以是一样的。在接收方使用这个字典时,为了避免内存泄漏,可以使用一个弱引用字典(WeakKeyDictionary)。

1

我不知道有没有现成的这种功能。这里有个有趣的问题,需要明确的规格来说明在这种情况下应该怎么处理……:

  • 进程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 模块。

2

我尝试实现了这个功能。@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!")

撰写回答