10

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

注意:注意:因为 WeakKeyDictionary 是建立在 Python 字典之上的,所以在迭代它时不能改变大小。这对于 WeakKeyDictionary 可能很难确保,因为程序在迭代期间执行的操作可能会导致字典中的项目“神奇地”消失(作为垃圾收集的副作用)。

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

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

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

import weakref

d = weakref.WeakKeyDictionary()

...

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

4 回答 4

8

在 Python 2.7 或 Python 3.1+ 中迭代 、 或 实际上是安全WeakKeyDictionaryWeakValueDictionary早在 2010 年,他们就设置了一个迭代保护,以防止弱引用回调从底层 dict 中删除引用或在迭代期间设置,但文档从未更新。WeakSet

使用警卫,如果一个条目在迭代到达它之前死亡,迭代将跳过该条目,但不会导致段错误或 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 中删除项目。(用 C 编写的事实list不会提供任何保护。)


回到 2.6 和更早版本,迭代 WeakKeyDictionary 或 WeakValueDictionary 的最安全方法是使用items. items将返回一个列表,它将使用底层 dict 的items方法,该方法(大多数情况下?)不会被 GC 中断。3.0 中 dict API 的变化改变了keys//的工作方式valuesitems这可能就是当初引入守卫的原因。

于 2018-10-07T23:05:57.267 回答
7

为了安全起见,您必须在某处保留参考。使用成语:

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

如果您使用 a WeakKeyDictionary,您可以简单地存储键,如果使用 ,则存储值WeakValueDictionary

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

最终,这取决于您所说的“安全”是什么意思。如果您只是意味着迭代将正确进行(对所有元素迭代一次),那么:

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

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

相反,如果您的意思是在迭代期间元素不应作为 -loop 的副作用从字典中“消失” for,那么您必须保持强引用直到循环结束,这需要您存储列表在开始循环之前在变量中。

于 2012-09-14T17:55:51.713 回答
1

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

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

如果它在 while 循环期间丢失了一些键,它不应该引起问题。

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

于 2019-01-31T05:36:50.487 回答
0

禁用垃圾收集器。

import gc

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

然后迭代items

于 2019-01-31T05:47:43.550 回答