python 搁置:重新打开搁置后相同的对象变成不同的对象

python shelve: same objects become different objects after reopening shelve

我在使用搁置时看到了这种行为:

import shelve

my_shelve = shelve.open('/tmp/shelve', writeback=True)
my_shelve['a'] = {'foo': 'bar'}
my_shelve['b'] = my_shelve['a']
id(my_shelve['a'])  # 140421814419392
id(my_shelve['b'])  # 140421814419392
my_shelve['a']['foo'] = 'Hello'
my_shelve['a']['foo']  # 'Hello'
my_shelve['b']['foo']  # 'Hello'
my_shelve.close()

my_shelve = shelve.open('/tmp/shelve', writeback=True)
id(my_shelve['a'])  # 140421774309128
id(my_shelve['b'])  # 140421774307832 -> This is weird.
my_shelve['a']['foo']  # 'Hello'
my_shelve['b']['foo']  # 'Hello'
my_shelve['a']['foo'] = 'foo'
my_shelve['a']['foo']  # 'foo'
my_shelve['b']['foo']  # 'Hello'
my_shelve.close()

如您所见,重新打开货架时,以前是同一对象的两个对象现在是两个不同的对象。

  1. 有人知道这里发生了什么吗?
  2. 有人知道如何避免这种行为吗?

我正在使用 Python 3.7.0

Anybody knows what is happening here?

Python 变量是对对象的引用。当您键入

a = 123

在幕后,Python 正在创建一个新对象 int(123),然后 a 指向它。如果你再写

a = 456

然后 Python 正在创建一个不同的对象 int(456),并更新 a 以成为对新对象的引用。它不会像 C 语言中的变量赋值那样覆盖存储在名为 a 的框中的内容。由于 id() returns 对象的内存地址(好吧,CPython 参考实现无论如何都会这样做),每次将 a 指向不同的对象时它都会有不同的值.

Anybody knows how to avoid this behavior?

你不能,因为这是 属性 赋值的工作方式。

shelve 将对象的 pickle 表示存储到 shelf 文件中。当您存储与 my_shelf['a']my_shelf['b'] 相同的对象时,shelve'a' 键写入一个对象的 pickle,为 [=15] 写入另一个对象的 pickle =] 键。需要注意的一件重要事情是它会单独腌制所有值。

当您重新打开架子时,shelve 使用腌制的表示来重建对象。它使用 'a' 的 pickle 重建您存储的字典,并使用 'b' 的 pickle 再次重建您存储的字典.

pickle 不会相互影响,并且在未 pickle 时无法 return 彼此相同的对象。在磁盘上的表示中没有迹象表明 my_shelf['a']my_shelf['b'] 曾经是同一个对象;使用 my_shelf['a']my_shelf['b'] 的单独对象制作的架子看起来可能完全相同。


如果您想保留这些对象相同的事实,则不应将它们存储在一个架子的单独键中。考虑使用 'a''b' 键而不是使用 shelve.

对单个字典进行 pickling 和 unpickling

有一种方法可以做到这一点,但它需要您自己制作 class,否则会变得更聪明。可以在pickle时注册原来的ids,设置unpickling函数,如果创建的对象被unpickle过,则查找它,如果没有,则创建它。

我有一个使用下面 __reduce__ 的简单示例。但是您应该首先知道这并不是最好的主意。

使用 copyreg 库可能更容易,但您应该知道,您使用此库所做的任何事情都会影响您一直 pickle 的任何内容。 __reduce__ 方法将更清晰、更安全,因为您明确告诉 pickle 您希望哪些 class 具有此行为,而不是将它们隐式应用于所有内容。

这个系统有更糟糕的警告。 id 将始终在 python 个实例之间更改,因此您需要在 __init__(或 __new__,无论您如何执行)期间存储原始 id,并确保保留现在已失效的值当它稍后被拉出货架时。由于垃圾收集,在 python 会话中甚至不能保证 id 的唯一性。我相信会出现其他不这样做的原因。 (我会尝试用我的 class 来解决这些问题,但我不做任何承诺。)

import uuid

class UniquelyPickledDictionary(dict):
    _created_instances = {}

    def __init__(self, *args, _uid=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.uid = _uid
        if _uid is None:
            self.uid = uuid.uuid4()
        UniquelyPickledDictionary._created_instances[self.uid] = self

    def __reduce__(self):
        return UniquelyPickledDictionary.create, (self.uid,), None, None, list(self.items())

    @staticmethod
    def create(uid):
        if uid in UniquelyPickledDictionary._created_instances:
            return UniquelyPickledDictionary._created_instances[uid]
        return UniquelyPickledDictionary(_uid=uid)

uuid 库应该比长 运行 中的对象 ID 更独特。我忘了他们持有什么保证,but I believe this is not multiprocessing safe

可以制作一个使用 copyreg 的等效版本来 pickle 任何 class,但需要对 unpickling 进行特殊处理以保证 repickling 指向同一对象。为了使其最通用,必须对 "already created" 字典进行检查以与所有实例进行比较。为了使其最可用,必须向实例添加一个新值,如果对象使用 __slots__(或在其他一些情况下),这可能是不可能的。

我使用的是 3.6,但我认为它应该适用于任何仍受支持的 Python 版本。它在我的测试中保留了对象,递归(但 pickle 已经这样做了)和多次 unpicklings。