Python 为什么在迭代过程中修改dict不会';你不总是提出例外吗?

Python 为什么在迭代过程中修改dict不会';你不总是提出例外吗?,python,python-3.x,dictionary,python-internals,Python,Python 3.x,Dictionary,Python Internals,在迭代过程中从中删除项通常会导致运行时错误:迭代过程中字典大小更改异常: d = {1: 2} # exception raised for k in d: del d[k] 更准确地说,删除本身将成功。但是,为了进入下一轮迭代,解释器必须调用next(it),其中it是通过先前获得的字典的迭代器。此时,next()将注意到字典大小已更改,并发出抱怨 到目前为止还不错。但是,如果我们同时删除一个条目并将其添加到字典中,该怎么办 d = {1: 1} # no exception raise

在迭代过程中从中删除项通常会导致
运行时错误:迭代过程中字典大小更改
异常:

d = {1: 2}
# exception raised
for k in d:
  del d[k]
更准确地说,删除本身将成功。但是,为了进入下一轮迭代,解释器必须调用
next(it)
,其中
it
是通过先前获得的字典的迭代器。此时,
next()
将注意到字典大小已更改,并发出抱怨

到目前为止还不错。但是,如果我们同时删除一个条目并将其添加到字典中,该怎么办

d = {1: 1}
# no exception raised
for k in d:
  # order of next two lines doesn't matter
  d[k*10] = k*10
  del d[k]
我几乎可以肯定这是不安全的(文档暗示在迭代期间既不允许插入也不允许删除)。为什么解释器允许此代码无错误地运行


我唯一的猜测是,每当调用insert或delete方法时,检查哪些迭代器无效的代价太高。因此,
dict
并不试图完美地提出这个异常。相反,它只是跟踪每个迭代器中字典的大小,并在实际要求迭代器移动到下一项时检查字典是否没有更改。是否没有能够以低成本实现完全验证的方法?

是否没有能够以低成本实现完全验证的方法?

以下是关于这个话题的相关报道

因为容器甚至不跟踪它上面的迭代器,更不用说钩住甚至改变方法来循环遍历每一个这样的迭代器,并以某种方式神奇地让每个迭代器知道这些改变。这将是一个非常微妙、复杂的代码,并且检查会减慢非常频繁的操作


因此,至少根据Python核心开发人员的说法,我们不能以低成本进行完整的验证

最简单的答案是,删除一个项目,然后添加一个项目,这样就不会发现大小已更改的事实;当迭代器的大小与迭代器的字典之间存在差异时,会引发
运行时错误

if (di->di_used != d->ma_used) {
    PyErr_SetString(PyExc_RuntimeError,
                    "dictionary changed size during iteration");
    di->di_used = -1; /* Make this state sticky */
    return NULL;
} 
当您添加一个并删除一个时,
di->di_used
d->ma_used
保持相同(该值增加1,减少1)。操作(
del
和key add)在
dict
对象
d
上执行,由于这些操作的平衡,在前面添加的
if
子句I中未发现不匹配

但是,例如,如果添加两个键,您将得到相同的错误:

d = {1: 1}
for k in d:
  del d[k]
  d[1] = 1
  d[2] = 2

RuntimeErrorTraceback (most recent call last)
<ipython-input-113-462571d7e0df> in <module>()
      1 d = {1: 1}
      2 # no exception raised
----> 3 for k in d:
      4   # order of next two lines doesn't matter
      5   del d[k]

RuntimeError: dictionary changed size during iteration
d={1:1}
对于d中的k:
德尔d[k]
d[1]=1
d[2]=2
RuntimeErrorTraceback(最近一次调用last)
在()
1d={1:1}
2#没有提出任何例外
---->3对于d中的k:
4#接下来两行的顺序无关紧要
5德尔d[k]
RuntimeError:字典在迭代期间更改了大小
因为意识到尺寸已经改变了,这一点在这里被抓住了。当然,如果减量两次,就会出现与以前相同的行为,这就平衡了

正如我在comment部分中迭代的那样,评估插入或删除是否以平衡的方式发生的检查并不像检查大小是否发生了变化那样简单。另外两个原因对我来说也没有意义:

  • 如果人们确实选择在迭代过程中修改词典,那么很可能他们不会以一种平衡的方式进行修改,所以在最常见的情况下,检入应该足够了
  • 如果您决定添加更多的检查,您将影响Python中几乎所有内容的性能(因为
    dict
    s无处不在)
总的来说,我怀疑增加这张支票会有好处;对大多数人来说,在更改集合的同时对集合进行迭代并不是最好的主意


像成年人一样,我们应该意识到Python不应该为我们检查一切,相反,当他们知道不必要的影响时,就不要做任何事情

确保在循环中尝试插入或删除密钥时引发异常的一种方法是保持对字典的修改次数。然后迭代器可以在其
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu

这段代码可以做到这一点。使用
SafeDict
或其
keys()
/
items()
/
values()
代理,循环不会意外插入/删除:

class SafeKeyIter:
    def __init__(self, iterator, container):
        self.iterator = iterator
        self.container = container
        try:
            self.n_modifications = container.n_modifications
        except AttributeError:
            raise RuntimeError('container does not support safe iteration')

    def __next__(self):
        if self.n_modifications != self.container.n_modifications:
            raise RuntimeError('container modified duration iteration')
        return next(self.iterator)

    def __iter__(self):
        return self


class SafeView:
    def __init__(self, view, container):
        self.view = view
        self.container = container

    def __iter__(self):
        return SafeKeyIter(self.view.__iter__(), self.container)

class SafeDict(dict):
    def __init__(self, *args, **kwargs):
        self.n_modifications = 0
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        if key not in self:
            self.n_modifications += 1
        super().__setitem__(key, value)

    def __delitem__(self, key):
        self.n_modifications += 1
        super().__delitem__(key)

    def __iter__(self):
        return SafeKeyIter(super().__iter__(), self)

    def keys(self):
        return SafeView(super().keys(), self)

    def values(self):
        return SafeView(super().values(), self)

    def items(self):
        return SafeView(super().items(), self)

# this now raises RuntimeError:
d = SafeDict({1: 2})
for k in d:
    d[k * 100] = 100
    del d[k]

这看起来并不太贵,所以我不确定为什么它没有在CPython
dict
中实现。也许在字典上更新
n\u修改的额外成本被认为太高了。

您是在寻找使循环更健壮的东西,还是想讨论Python实现细节?看起来您希望字典键在循环中不可变。我认为这是行不通的。嗯,我想两者都有吧?如果有一种技术可以做到这一点,我会考虑自己使用它。但为了了解它的成本(运行时、代码复杂性等),了解CPython为什么不使用它对我来说很重要。@DYZ不可变键比我要求的要强大得多。我不想仅仅因为给定键的值被更改而引发异常-在循环中这样做是完全安全的(事实上,如果不是python,它将是一种坏语言!)@DYZ Btw,制作一个不可变版本的
dict
在python中作为内置功能提供:
immutable\u d=types.MappingProxyType(d)
。如果不需要任何修改,那么在循环中使用它将是安全的。当然,使用原始的(可变的)
d
仍然可能会弄糟。无论如何,这是我需要的一个更强的限制。嗯,我想Alex Martelli是指在迭代时允许修改字典的困难。这比检测修改要困难得多