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



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

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

shelve将对象的pickled表示存储到工具架文件中。当存储与my_shelf['a']my_shelf['b']相同的对象时,shelve会为'a'键写入对象的pickle,为'b'键写入另一个pickle。需要注意的一个关键点是,它单独提取所有值。

当您重新打开工具架时,shelve会使用腌制的表示来重建对象。它使用'a'的pickle来重构您存储的dict,并使用'b'的pickles来重构您再次存储的dict

泡菜彼此不交互,并且在取消拾取时无法返回彼此相同的对象。在盘上表示中没有指示my_shelf['a']my_shelf['b']曾经是同一对象;使用CCD_ 12和CCD_。


如果你想保留这些对象是相同的事实,你不应该把它们存储在架子的单独键中。考虑使用'a''b'键而不是使用shelve对单个dict进行pickle和unpickle。

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

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

a = 123

在幕后,Python正在创建一个新对象int(123),然后使a指向它

a = 456

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

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

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

有一种方法可以做到这一点,但它需要您创建自己的类,或者变得更聪明。您可以在pickle时注册原始id,并设置unpickle函数来查找已创建的对象(如果已取消pickle(,或者创建它(如果尚未创建(。

下面我有一个使用__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更唯一。我忘了它们有什么保证,但我认为这不是多处理安全的。

使用copyreg的等效版本可以pickle任何类,但需要对取消pickle进行特殊处理,以确保重新pickle指向同一对象。为了使其最通用,必须对"已创建"的字典进行检查,以与所有实例进行比较。为了使其最可用,必须向实例添加一个新值,如果对象使用__slots__(或在其他一些情况下(,则这可能是不可能的。

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

最新更新