对WeakKeyDictionary和WeakValueDictionary进行安全迭代



Python 3.2的weakref模块的WeakKeyDictionaryWeakValueDictionary的文档中有一个关于迭代这些容器的注释:

注意:警告:因为WeakKeyDictionary是在Python字典的基础上构建的,所以在对其进行迭代时,它不能更改大小。这对于WeakKey字典来说可能很难确保,因为程序在迭代过程中执行的操作可能会导致字典中的项"魔术般"消失(这是垃圾收集的副作用)。

作为这些容器行为的规范,这似乎相当可怕。特别是当运行使用CPython垃圾收集器的代码时(当使用包含循环的数据结构时)或使用另一个Python实现(例如Jython),那么听起来似乎没有安全的方法来迭代这些集合。

当垃圾收集器可以在我的程序中的任何时候清除引用时,我如何安全地迭代这些集合?为CPython提供解决方案是我的首要任务,但我对其他实现中的问题也很感兴趣。

这可能是一种对WeakKeyDictionary进行迭代的安全方法吗?

import weakref
d = weakref.WeakKeyDictionary()
...
for k, v in list(d.items()):
    ...

在Python 2.7或Python 3.1+中对WeakKeyDictionaryWeakValueDictionaryWeakSet进行迭代实际上是安全的。早在2010年,他们就引入了迭代保护,防止weakref回调在迭代过程中从底层dict或集合中删除引用,但文档从未更新。

有了保护,如果一个条目在迭代到达之前死亡,迭代将跳过该条目,但不会导致segfault或RuntimeError或其他任何情况。死条目将被添加到挂起删除的列表中,稍后进行处理。

以下是防护装置(尽管有评论,但并不安全):

class _IterationGuard:
    # This context manager registers itself in the current iterators of the
    # weak container, such as to delay all removals until the context manager
    # exits.
    # This technique should be relatively thread-safe (since sets are).
    def __init__(self, weakcontainer):
        # Don't create cycles
        self.weakcontainer = ref(weakcontainer)
    def __enter__(self):
        w = self.weakcontainer()
        if w is not None:
            w._iterating.add(self)
        return self
    def __exit__(self, e, t, b):
        w = self.weakcontainer()
        if w is not None:
            s = w._iterating
            s.remove(self)
            if not s:
                w._commit_removals()

以下是WeakKeyDictionary weakref回调检查保护的位置:

def remove(k, selfref=ref(self)):
    self = selfref()
    if self is not None:
        if self._iterating:
            self._pending_removals.append(k)
        else:
            del self.data[k]

这里是WeakKeyDictionary.__iter__设置保护的地方:

def keys(self):
    with _IterationGuard(self):
        for wr in self.data:
            obj = wr()
            if obj is not None:
                yield obj
__iter__ = keys

在其他迭代器中使用相同的保护。


如果这个保护不存在,调用list(d.items())也不安全。GC传递可能发生在items迭代器内部,并在迭代期间从dict中删除项。(事实上,list是用C编写的,这不会提供任何保护。)


早在2.6及更早版本中,迭代WeakKeyDictionary或WeakValueDictionary最安全的方法是使用itemsitems将返回一个列表,并且它将使用底层dict的items方法,该方法(大部分?)不会被GC中断。3.0中的dict API更改改变了keys/values/items的工作方式,这可能就是当初引入保护的原因。

为了安全起见,您必须在某个地方保留一个引用。使用成语:

for k,v in list(d.items()):

并不完全安全,因为即使它在大多数情况下都能工作,但在循环的最后一次迭代过程中,列表可能会被垃圾收集。

正确的方法是:

items = list(d.items())
for k,v in items:
    #do stuff that doesn't have a chance of destroying "items"
del items

如果使用WeakKeyDictionary,则可以简单地存储密钥,如果使用WeakValueDictionary,则可以存储值。

附带说明:在python2.items()中已经返回了一个列表。

归根结底,这取决于你所说的"安全"是什么意思。如果你只是说迭代将正确进行(在所有元素上迭代一次),那么:

for k,v in list(d.items()):

是安全的,因为字典上的迭代实际上是由list(d.items())执行的,所以您只是在列表上迭代。

相反,如果您的意思是,在迭代过程中,元素不应该作为for-循环的副作用从字典中"消失",那么您必须保留一个强引用,直到循环结束,这要求您在开始循环之前将列表存储在一个变量中。

在不首先使用迭代的情况下转换为强引用。

items = []
while d:
    try:
        items.append(d.popitem())
    except KeyError:
        pass

如果它在while循环中丢失了一些键,那么应该不会引起问题。

然后您可以迭代items。完成后,用d.update(items)将它们放回原处,然后用del items

禁用垃圾收集器。

import gc
gc.disable()
try:
    items = list(d.items())
finally:
    gc.enable()

然后迭代items

最新更新