蟒蛇搁置:重新打开搁置后,相同的对象变为不同的对象

时间:2018-11-05 23:57:18

标签: python pickle python-internals python-3.7 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

3 个答案:

答案 0 :(得分:1)

shelve将对象的腌制表示存储到架子文件中。当您存储与my_shelf['a']my_shelf['b']相同的对象时,shelve会为'a'键写一个对象的泡菜,并为{{1 }}键。要注意的一件事是它会分别腌制所有值。

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

泡菜彼此之间没有交互作用,并且没有任何方式可以使泡菜在未泡菜时彼此返回。磁盘上的表示形式中没有指示'b'my_shelf['a']曾经是同一对象。使用my_shelf['b']my_shelf['a']的不同对象生产的架子看起来相同。


如果要保留这些对象相同的事实,则不应将它们存储在架子的单独键中。考虑使用my_shelf['b']'a'键而不是使用'b'腌制和解开单个字典。

答案 1 :(得分:0)

  

有人知道这里发生了什么吗?

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

a = 123

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

a = 456

然后Python将创建一个不同的对象int(456),并将a更新为对新对象的引用。它不会像使用C语言中的变量赋值那样覆盖名为a的框中存储的内容。由于id()返回对象的内存地址(嗯,无论如何,CPython参考实现都会执行此操作),所以每次将a指向不同的对象时,它将具有不同的值。

  

有人知道如何避免这种行为吗?

您不能,因为这是分配工作原理的一个属性。

答案 2 :(得分:0)

有一种方法可以做到这一点,但是这将需要您上自己的课,或者变得聪明。您可以在腌制过程中注册原始ID,并设置取消腌制功能以在创建的对象未腌制的情况下查找创建的对象,或者在未腌制的对象进行创建。

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

使用copyreg库可能会更容易,但是您应该知道,对此库所做的任何事情都会一直影响您腌制的任何东西。当您明确告诉__reduce__您希望哪些类具有此行为,而不是隐式地将它们应用于所有内容时,pickle方法将更加干净和安全。

对该系统有更糟糕的警告。 id在python实例之间总是会变化,因此您需要在__init__(或__new__,但可以这样做)期间存储原始id,并确保拔出时现在保留了已失效的值搁置以后。由于垃圾回收,在python会话中甚至无法保证id的唯一性。我敢肯定还会有其他不这样做的原因。 (我将在课堂上尝试解决这些问题,但我没有做出任何承诺。)

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的等效版本腌制任何类,但是需要对解腌过程进行特殊处理,以确保重新腌制指向同一对象的点。为了使其最通用,必须对“已经创建”的字典进行检查以与所有实例进行比较。为了使其最有用,必须向实例添加一个新值,如果对象使用__slots__(或在其他一些情况下),则可能无法实现。

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