【问题标题】:python shelve: same objects become different objects after reopening shelvepython搁置:重新打开搁置后相同的对象变成不同的对象
【发布时间】:2018-11-05 23:57:18
【问题描述】:

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

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

【问题讨论】:

  • 为什么需要避免这种行为?
  • @EdgarR.Mondragón 当我试图腌制一棵树搁置时,我的问题出现了。这棵树有一堆指向树中其他节点的节点。

标签: python pickle python-internals python-3.7 shelve


【解决方案1】:

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

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

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


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

【讨论】:

    【解决方案2】:

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

    Python 变量是对对象的引用。当你输入

    a = 123

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

    a = 456

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

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

    你不能,因为它是分配工作方式的一个属性。

    【讨论】:

    • 这只是部分正确,与shelve的机制有关的问题几乎没有关系。不同的持久性机制可以很容易地保留同一对象存储到 'a''b' 键的事实。
    • 啊,我明白你现在在说什么了。不过,我不会说“容易”,除非该过程的一部分是构建对象 id 的映射,以便在引用同一对象之前执行相同操作的多个键之后执行相同操作。如果持久化对象实现描述符协议,这听起来非常昂贵。
    【解决方案3】:

    有一种方法可以做到这一点,但这需要你自己开课,或者变得聪明。您可以在 pickling 时注册原始 id,并设置一个 unpickling 函数来查找创建的对象,如果它已经 unpickled,或者如果它没有创建它。

    我有一个使用下面__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 已经这样做了)和多个 unpicklings。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-12
      • 2011-07-13
      相关资源
      最近更新 更多