如何避免出现“RuntimeError: dictionary changed size during iteration”错误?

501
假设我有一个列表的字典:
d = {'a': [1], 'b': [1, 2], 'c': [], 'd':[]}

现在我想要删除值为空列表的键值对。我尝试了以下代码:
for i in d:
    if not d[i]:
        d.pop(i)

但是这会导致一个错误:
RuntimeError: dictionary changed size during iteration

我知道在遍历字典时无法添加或删除条目。为了解决这个问题,我该如何绕过这个限制?

请参阅在迭代过程中修改Python字典,以了解可能导致问题的引用和原因。


除了下面给出的答案之外,您可能需要在从列表中删除元素的地方检查空列表。 您可以编写一个辅助函数:def remove_helper(d,k,elem):d [k] .remove(elem); 如果不是d [k]:del d [k] - joseville
从实际角度来看,可能有三种有意义的不同方法来做到这一点,并且在我的评估中,对这些方法的每一个有意义的微小变化最多可以通过考虑这里的6个答案来涵盖。令人有些担忧的是,还有8个未删除的答案,以及另外5个之前被删除的答案。 - Karl Knechtel
@joseville 为了明确起见:您建议在列表变为空时更新字典,而不是迭代删除空列表?这可能是一个实际的方法来解决上下文中更广泛的问题,具体取决于原始要求的确切情况。但我同意这并不足以证明需要单独回答。 - Karl Knechtel
15个回答

778
在 Python 3.x 和 2.x 中,您可以使用 list 来强制复制键:
for i in list(d):

在Python 2.x中调用.keys会复制可迭代的键,同时修改dict
for i in d.keys():

但在Python 3.x上,.keys返回一个视图对象,所以它无法修复你的错误。

1
我相信你的意思是“调用keys会复制你可以迭代的键(keys)”,也就是复数的键,对吗?否则,如何迭代单个键呢?顺便说一下,我并不是在挑剔,我真的很想知道这是否确实是键还是键。 - AjB
11
使用元组而不是列表,因为它更快。 - Brambor
24
为了澄清Python 3.x的行为,d.keys() 返回一个可迭代对象(而不是迭代器),这意味着它直接是字典键的视图。在Python 3.x中通常情况下使用 for i in d.keys() 实际上是可以工作的,但是由于它正在迭代字典键的可迭代视图,在循环期间调用 d.pop() 导致与您发现的相同的错误。for i in list(d) 模拟了稍微低效的Python 2行为,即在迭代之前将键复制到列表中,以应对特殊情况(例如你所面临的情况)。 - Michael Krebs
8
在Python3.x中,list(d.keys())list(d)的输出结果相同。在字典上调用list会返回字典的键。虽然调用keys()方法不会太耗费资源,但却是不必要的。 - Sean Breckenridge
1
@DanielChin:那份文档是针对错误的Python版本的。请参阅https://docs.python.org/3/library/stdtypes.html#dict.items。 - Ry-
显示剩余2条评论

128

你只需要使用copy

这样,你可以迭代原始字典字段,并即时更改所需的字典d。 它适用于每个Python版本,因此更加清晰。

In [1]: d = {'a': [1], 'b': [1, 2], 'c': [], 'd':[]}

In [2]: for i in d.copy():
   ...:     if not d[i]:
   ...:         d.pop(i)
   ...:         

In [3]: d
Out[3]: {'a': [1], 'b': [1, 2]}

(顺便说一下 - 通常情况下,如果要迭代复制您的数据结构,不要使用字典的.copy或列表的切片[:],可以使用import copy -> copy.copy(浅层复制相当于字典支持的copy或列表支持的[:] )或在数据结构上使用copy.deepcopy。)


8
对于字典中的每个键和值,使用.copy().items()方法: - Aseem

69

只需使用字典推导式将相关项复制到新字典中:

>>> d
{'a': [1], 'c': [], 'b': [1, 2], 'd': []}
>>> d = {k: v for k, v in d.items() if v}
>>> d
{'a': [1], 'b': [1, 2]}

对于Python 2中的这个问题:

>>> d
{'a': [1], 'c': [], 'b': [1, 2], 'd': []}
>>> d = {k: v for k, v in d.iteritems() if v}
>>> d
{'a': [1], 'b': [1, 2]}

12
"d.iteritems()" 给了我一个错误。我使用了 "d.items()" 替代它 - 使用 Python3。 - wcyn
9
这个方法适用于OP问题中提出的情况。然而,对于在多线程代码中遇到此RuntimeError的任何人,请注意CPython的全局解释锁(GIL)也可能在列表推导式运行过程中被释放,你需要采用不同的方法来解决它。 - Yirkha
字典推导式和列表推导式一样吗?字典推导式似乎不太流行(或在官方文档中没有提到)。有PEP 274,但它是否被实现了呢? - Peter Mortensen

39

这对我有用:

d = {1: 'a', 2: '', 3: 'b', 4: '', 5: '', 6: 'c'}
for key, value in list(d.items()):
    if value == '':
        del d[key]
print(d)
# {1: 'a', 3: 'b', 6: 'c'}
将字典项转换为列表可以创建一个包含其所有项的列表,因此您可以对其进行迭代并避免 RuntimeError 。

15
为避免“在迭代过程中改变了字典大小”的错误。例如:“当您尝试删除某个键时”,只需使用带有“.items()”的“列表”。以下是一个简单的示例:
my_dict = {
    'k1':1,
    'k2':2,
    'k3':3,
    'k4':4
 
    }
    
print(my_dict)

for key, val in list(my_dict.items()):
    if val == 2 or val == 4:
        my_dict.pop(key)

print(my_dict)

输出:

{'k1': 1, 'k2': 2, 'k3': 3, 'k4': 4}

{'k1': 1, 'k3': 3}

这只是一个例子。根据您的情况/要求进行更改。


虽然我不太理解原因,但这对我有效。 - lam vu Nguyen
这种方法已经在2020年的singrium的回答中涵盖了,并且是2012年原始答案中其他方法的一个微不足道的变体。 - Karl Knechtel

15

我会尽量避免一开始就插入空列表,但通常会使用:

d = {k: v for k,v in d.iteritems() if v} # re-bind to non-empty

如果是在2.7之前:

d = dict( (k, v) for k,v in d.iteritems() if v )

或者只需:

empty_key_vals = list(k for k in k,v in d.iteritems() if v)
for k in empty_key_vals:
    del[k]

+1:最后一个选项很有趣,因为它只复制需要删除的那些项目的键。如果相对于字典的大小只有少量项目需要删除,则可能会提供更好的性能。 - Mark Byers
@MarkByers 是的 - 如果有大量这样的操作,那么重新绑定字典到一个经过筛选的新字典是更好的选择。这总是对结构如何工作的期望。 - Jon Clements
4
重新绑定的一个危险是,如果程序中有一个对象持有旧字典的引用,那么它就无法看到更改。如果你确定这种情况不会发生,那么这是一个合理的方法,但重要的是要理解它与修改原始字典并不完全相同。 - Mark Byers
@MarkByers非常好的观点 - 你和我知道这一点(以及无数其他人),但并不明显。我敢打赌,这也没有让你吃过亏 :) - Jon Clements
避免插入空条目的观点非常好。 - Magnus Bodin

12

对于Python 3:

{k:v for k,v in d.items() if v}

很好,简洁明了。在Python 2.7中也适用。 - ron_g
这个方法已经在原始答案中涵盖了。 - Karl Knechtel

8

for 循环期间更改字典时,无法遍历该字典。将其强制转换为列表并遍历该列表。这对我有效。

    for key in list(d):
        if not d[key]:
            d.pop(key)

这个方法已经在原始答案中提到了。 - Karl Knechtel

2
使用一个列表来收集应该被移除的键;然后在遍历列表时使用pop字典方法来移除已识别的键(这是一个独立的对象,所以错误不会发生)。
d = {'a': [1], 'b': [1, 2], 'c': [], 'd':[]}
pop_list = []

for i in d:
    if not d[i]:
        pop_list.append(i)

for x in pop_list:
    d.pop(x)

print(d)

这个技巧在Jon Clements 2012年的回答中提到过,但是使用了一个理解来进行第一次循环。 - Karl Knechtel

2

在Python 3中,使用上述for循环迭代字典时不允许进行删除操作。有各种替代方法可以解决这个问题;其中一种简单的方法是更改该行代码。

for i in x.keys():

使用

for i in list(x)

这个方法已经在原始答案中涵盖了。 - Karl Knechtel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接