【问题标题】:Modifying a Python dict while iterating over it迭代 Python 字典时修改它
【发布时间】:2011-10-10 06:53:01
【问题描述】:

假设我们有一个 Python 字典 d,我们像这样迭代它:

for k,v in d.iteritems():
    del d[f(k)] # remove some item
    d[g(k)] = v # add a new item

fg 只是一些黑盒转换。)

换句话说,我们尝试向d 添加/删除项目,同时使用iteritems 对其进行迭代。

这是明确定义的吗?您能否提供一些参考资料来支持您的回答?

(很明显,如果它坏了如何解决这个问题,所以这不是我想要的角度。)

【问题讨论】:

标签: python dictionary


【解决方案1】:

您不能这样做,至少对于 d.iteritems()。我试过了,Python 失败了

RuntimeError: dictionary changed size during iteration

如果您改为使用d.items(),那么它可以工作。

在 Python 3 中,d.items() 是字典的视图,类似于 Python 2 中的 d.iteritems()。要在 Python 3 中执行此操作,请改用 d.copy().items()。这同样允许我们迭代字典的副本,以避免修改我们正在迭代的数据结构。

【讨论】:

  • 仅供参考,Py2 的 d.items() 到 Py3 的直译(例如 2to3 使用的)是 list(d.items()),尽管 d.copy().items() 的效率可能相当。
  • 如果dict对象很大,d.copy().items()是否有效?
【解决方案2】:

下面的代码表明这不是很好的定义:

def f(x):
    return x

def g(x):
    return x+1

def h(x):
    return x+10

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[g(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[h(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

第一个示例调用 g(k),并抛出异常(迭代期间字典大小改变)。

第二个例子调用 h(k) 并没有抛出异常,但是输出:

{21: 'axx', 22: 'bxx', 23: 'cxx'}

查看代码,这似乎是错误的 - 我本来希望是这样的:

{11: 'ax', 12: 'bx', 13: 'cx'}

【讨论】:

  • 我能理解你为什么会期待{11: 'ax', 12: 'bx', 13: 'cx'},但是 21,22,23 应该可以让你知道实际发生了什么:你的循环遍历了第 1、2、3、11、12 项, 13 但没有设法拾取第二轮新项目,因为它们被插入到您已经迭代过的项目前面。将h() 更改为返回x+5,你会得到另一个x:'axxx' 等或'x+3',你会得到华丽的'axxxxx'
  • 是的,我担心我的错误 - 正如你所说,我的预期输出是 {11: 'ax', 12: 'bx', 13: 'cx'},所以我会更新我的帖子。无论哪种方式,这显然都不是明确定义的行为。
【解决方案3】:

Alex Martelli 对此here 发表意见。

在循环容器时更改容器(例如 dict)可能不安全。 所以del d[f(k)] 可能不安全。如您所知,解决方法是使用d.items()(循环遍历容器的独立副本)而不是d.iteritems()(使用相同的底层容器)。

可以在字典的现有索引处修改值,但在新索引处插入值(例如d[g(k)]=v)可能不起作用。

【讨论】:

  • 我认为这对我来说是一个关键的答案。许多用例将有一个进程插入东西,另一个进程清理/删除它们,因此使用 d.items() 的建议有效。 Python 3 警告无法承受
  • 有关 Python 3 警告的更多信息可以在 PEP 469 中找到,其中列举了上述 Python 2 dict 方法的语义等价物。
  • “可以在字典的现有索引处修改值” -- 你有这个参考吗?
  • @JonathonReinhart:不,我没有这方面的参考资料,但我认为它在 Python 中是相当标准的。例如,Alex Martelli 是一名 Python 核心开发人员,demonstrates its usage here
【解决方案4】:

在 Python 文档页面(Python 2.7)上明确提到

在字典中添加或删除条目时使用iteritems() 可能会引发RuntimeError 或无法遍历所有条目。

Python 3 也是如此。

iter(d)d.iterkeys()d.itervalues()如果实现调用iter(d) 不会感到惊讶)。

【讨论】:

  • 为了社区的利益,我会说我使用了代码 sn-p,这让我自己感到尴尬。认为由于我没有收到 RuntimeError 我认为一切都很好。有一段时间了。 Anally 保持性单元测试让我赞不绝口,它甚至在发布时运行良好。然后,我开始出现奇怪的行为。发生的情况是字典中的项目被跳过,因此并非字典中的所有项目都被扫描。孩子们,从我一生中犯下的错误中吸取教训,然后说不! ;)
  • 如果我要更改当前键的值(但不添加或删除任何键),我会遇到问题吗?我认为这不会导致任何问题,但我想知道!
  • @GershomMaes 我不知道,但是如果您的循环体使用该值并且不希望它改变,您可能仍然会遇到雷区。
  • d.items() 在 Python 2.7 中应该是安全的(Python 3 改变了游戏规则),因为它本质上是 d 的副本,所以你不会修改你正在迭代的内容结束了。
  • 想知道viewitems()是否也是如此
【解决方案5】:

我遇到了同样的问题,我使用以下程序解决了这个问题。

Python List 可以被迭代,即使你在迭代它的过程中进行了修改。 因此对于以下代码,它将无限打印 1。

for i in list:
   list.append(1)
   print 1

所以list和dict协同使用就可以解决这个问题。

d_list=[]
 d_dict = {} 
 for k in d_list:
    if d_dict[k] is not -1:
       d_dict[f(k)] = -1 # rather than deleting it mark it with -1 or other value to specify that it will be not considered further(deleted)
       d_dict[g(k)] = v # add a new item 
       d_list.append(g(k))

【讨论】:

  • 我不确定在迭代期间修改列表是否安全(尽管在某些情况下它可能有效)。例如,请参阅this question...
  • @Roman 如果你想删除一个列表的元素,你可以安全地以相反的顺序迭代它,因为在正常顺序下,下一个元素的索引会在删除时改变。 See this example.
【解决方案6】:

我有一个包含 Numpy 数组的大字典,所以 @murgatroid99 建议的 dict.copy().keys() 东西是不可行的(尽管它有效)。相反,我只是将 keys_view 转换为列表,它工作正常(在 Python 3.4 中):

for item in list(dict_d.keys()):
    temp = dict_d.pop(item)
    dict_d['some_key'] = 1  # Some value

我意识到这并没有像上面的答案那样深入 Python 内部工作的哲学领域,但它确实为所述问题提供了实用的解决方案。

【讨论】:

  • 请注意,dict.copy() 不是深层副本,因此您的 dict 中的内容并不重要,因为不会复制这些值。
【解决方案7】:

今天我有一个类似的用例,但不是简单地在循环开始时将字典上的键具体化,而是希望更改字典以影响字典的迭代,这是一个有序字典。

我最终构建了以下例程,也可以是found in jaraco.itertools

def _mutable_iter(dict):
    """
    Iterate over items in the dict, yielding the first one, but allowing
    it to be mutated during the process.
    >>> d = dict(a=1)
    >>> it = _mutable_iter(d)
    >>> next(it)
    ('a', 1)
    >>> d
    {}
    >>> d.update(b=2)
    >>> list(it)
    [('b', 2)]
    """
    while dict:
        prev_key = next(iter(dict))
        yield prev_key, dict.pop(prev_key)

文档字符串说明了用法。这个函数可以用来代替上面的d.iteritems()来达到预期的效果。

【讨论】:

    【解决方案8】:

    Python 3 你应该:

    prefix = 'item_'
    t = {'f1': 'ffw', 'f2': 'fca'}
    t2 = dict() 
    for k,v in t.items():
        t2[k] = prefix + v
    

    或使用:

    t2 = t1.copy()
    

    你不应该修改原始字典,它会导致混乱以及潜在的错误或 RunTimeErrors。除非你只是用新的键名附加到字典中。

    【讨论】:

      【解决方案9】:

      这个问题是关于使用迭代器(很有趣,Python 3 不再支持 Python 2 .iteritems 迭代器) 来删除或添加项目,并且 它必须有No 作为唯一正确答案,您可以在接受的答案中找到它。然而:大多数搜索者试图找到一个解决方案,他们不会关心这是如何在技术上完成的,无论是迭代器还是递归,解决问题的方法:

      您不能在不使用附加(递归)函数的情况下循环更改字典。

      因此,该问题应与具有有效解决方案的问题相关联:

      通过相同的递归方法,您还可以根据问题的要求添加项目。


      由于我链接此问题的请求被拒绝,这里是可以从字典中删除项目的解决方案的副本。请参阅 How can I remove a key:value pair wherever the chosen key occurs in a deeply nested dictionary? (= "delete") 获取示例/学分/注释。

      import copy
      
      def find_remove(this_dict, target_key, bln_overwrite_dict=False):
          if not bln_overwrite_dict:
              this_dict = copy.deepcopy(this_dict)
      
          for key in this_dict:
              # if the current value is a dict, dive into it
              if isinstance(this_dict[key], dict):
                  if target_key in this_dict[key]:
                      this_dict[key].pop(target_key)
      
                  this_dict[key] = find_remove(this_dict[key], target_key)
      
          return this_dict
      
      dict_nested_new = find_remove(nested_dict, "sub_key2a")
      

      诀窍

      诀窍是在递归到达子级别之前提前找出 target_key 是否在下一个子级中(= this_dict[key] = 当前 dict 迭代的值)。只有这样,您仍然可以在遍历字典时删除子级别的键:值对。一旦您达到与要删除的密钥相同的级别,然后尝试从那里删除它,您将收到错误消息:

      RuntimeError: dictionary changed size during iteration
      

      递归解决方案仅对下一个值的子级别进行任何更改,因此避免了错误。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-07-01
        • 1970-01-01
        • 1970-01-01
        • 2013-02-22
        • 2021-03-30
        • 2023-04-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多