Python 3.2 的weakref
模块WeakKeyDictionary
和WeakValueDictionary
的文档中有关于迭代这些容器的注释:
注意:警告:因为WeakKeyDictionary是在Python字典之上构建的,所以在迭代它时不能改变大小。这对于WeakKeyDictionary来说可能很难确保,因为程序在迭代期间执行的操作可能会导致字典中的项目“通过魔法”消失(作为垃圾收集的副作用)。
这似乎是对这些容器行为的规范相当可怕。特别是当运行使用CPython的垃圾收集器的代码(当使用包含循环的数据结构时)或使用另一个Python实现(例如Jython)时,听起来似乎没有安全的方法来迭代这些集合。
当垃圾收集器可以在程序中的任何位置清除引用时,如何安全地迭代这些集合?有一个CPython的解决方案是我的首要任务,但我也对其他实现的问题感兴趣。
这可能是一种迭代WeakKeyDictionary的安全方法吗?
import weakref
d = weakref.WeakKeyDictionary()
...
for k, v in list(d.items()):
...
答案 0 :(得分: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
如果您使用WeakKeyDictionary
,则只需存储密钥,并在使用WeakValueDictionary
时存储值。
旁注:在python2中.items()
已经返回一个列表。
最终,这取决于你所说的“安全”。如果您只是意味着迭代将正确进行(在所有元素上迭代一次),那么:
for k,v in list(d.items()):
是安全的,因为字典上的迭代实际上是由list(d.items())
执行的,那么你只是遍历列表。
相反,如果你意味着在迭代过程中元素不应该从字典中“消失”作为for
- 循环的副作用,那么你必须保持一个强引用,直到循环结束,这需要您在开始循环之前将列表存储在变量中。
答案 1 :(得分:4)
在Python 2.7或Python 3.1+中,实际上可以安全地遍历WeakKeyDictionary
,WeakValueDictionary
或WeakSet
。 They put in an iteration guard可以防止弱引用回调在2010年的迭代过程中从底层dict或集合中删除引用,但是文档从未得到更新。
在加入警戒的情况下,如果条目在迭代到达之前死亡,则迭代将跳过该条目,但不会导致segfault或RuntimeError或任何其他错误。无效条目将添加到待处理的删除列表中,并在以后进行处理。
Here's the guard(尽管有评论,但不是线程安全的):
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()
Here's where the WeakKeyDictionary weakref callback checks the guard:
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]
And here's where WeakKeyDictionary.__iter__
sets the guard:
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的最安全方法是使用items
。 items
将返回一个列表,并且将使用基础dict的items
方法,该方法(通常是?)不会被GC中断。 dict API在3.0中进行了更改,从而更改了keys
/ values
/ items
的工作方式,这很可能就是在引入防护后的原因。
答案 2 :(得分:1)
无需先使用迭代即可转换为强引用。
items = []
while d:
try:
items.append(d.popitem())
except KeyError:
pass
如果它在while循环中丢失了一些键,就不会造成问题。
然后,您可以改为遍历items
。完成后,d.update(items)
将它们放回去,然后del items
。
答案 3 :(得分:0)
禁用垃圾收集器。
import gc
gc.disable()
try:
items = list(d.items())
finally:
gc.enable()
然后遍历items
。