安全地迭代WeakKeyDictionary和WeakValueDictionary

Feu*_*mel 9 python dictionary weak-references

的文档的Python 3.2weakref模块WeakKeyDictionary,并WeakValueDictionary有一张纸条上遍历这些容器:

注意:注意:因为WeakKeyDictionary是在Python字典之上构建的,所以在迭代它时不能改变大小.这对于WeakKeyDictionary来说可能很难确保,因为程序在迭代期间执行的操作可能会导致字典中的项目"通过魔法"消失(作为垃圾收集的副作用).

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

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

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

import weakref

d = weakref.WeakKeyDictionary()

...

for k, v in list(d.items()):
    ...
Run Code Online (Sandbox Code Playgroud)

Bak*_*riu 7

为了安全起见,你必须在某个地方保留一个参考.使用成语:

for k,v in list(d.items()):
Run Code Online (Sandbox Code Playgroud)

并不是完全安全的,因为即使它在大多数时间都可以工作,但在循环的最后一次迭代中,列表可能是垃圾收集的.

正确的方法是:

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

如果使用a WeakKeyDictionary,则可以只存储密钥,并在使用时存储值WeakValueDictionary.

旁注:在python2中.items()已经返回一个列表.

最终,这取决于你所说的"安全".如果您只是意味着迭代将正确进行(在所有元素上迭代一次),那么:

for k,v in list(d.items()):
Run Code Online (Sandbox Code Playgroud)

是安全的,因为字典上的迭代实际上是由执行的list(d.items()),那么你只是遍历列表.

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

  • 我可能误解了一些东西,但是如何删除对象`k`和`​​v`引用?只要这些变量在范围内而不是覆盖,引用的对象就是安全的.或者您是在谈论在上一次迭代期间删除对这些对象的所有强引用?这将改变字典但不会不安全,因为迭代开始后不会访问字典.你能举例说明上一次迭代中可能出现的问题吗? (3认同)
  • 为什么你的第一个例子不安全?该列表将包含对每个键和值的强引用,并且在最后一次迭代中,`k`和`​​v`持有对我感兴趣的对象的强引用.因此,即使在最后一次迭代终止之前,列表也可能被垃圾收集.是对的吗? (2认同)

use*_*ica 5

这是在实际安全的迭代WeakKeyDictionaryWeakValueDictionaryWeakSet在Python 2.7或Python 3.1+。他们引入了一个迭代保护措施,以防止弱引用回调从2010年以来的迭代过程中从底层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()
Run Code Online (Sandbox Code Playgroud)

这是WeakKeyDictionary弱引用回调检查防护的地方

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]
Run Code Online (Sandbox Code Playgroud)

这是WeakKeyDictionary.__iter__设置警卫的地方

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

__iter__ = keys
Run Code Online (Sandbox Code Playgroud)

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


如果没有这个警卫,打电话list(d.items())也不安全。GC遍历可能发生在items迭代器内部,并在迭代过程中从dict中删除项目。(list用C语言编写的事实不会提供任何保护。)


在2.6及更早版本中,迭代WeakKeyDictionary或WeakValueDictionary的最安全方法是使用itemsitems会返回一个列表,并使用底层dict的items方法,该方法通常不会被GC中断(主要是?)。在3.0的字典API的变化而变化如何keys/ values/ items工作,这可能是为什么警卫被引入时,它是。