自定义dict,允许在迭代期间删除

时间:2012-01-26 18:12:10

标签: python dictionary iterator python-3.x

根据Lennart Regebro的回答更新

假设您遍历字典,有时需要删除元素。以下是非常有效的:

remove = []
for k, v in dict_.items():
  if condition(k, v):
    remove.append(k)
    continue
  # do other things you need to do in this loop
for k in remove:
  del dict_[k]

这里唯一的开销是构建要删除的密钥列表;除非它与字典大小相比变大,否则不是问题。但是,这种方法需要一些额外的编码,所以它不是很受欢迎。

流行的字典理解方法:

dict_ = {k : v for k, v in dict_ if not condition(k, v)}
for k, v in dict_.items():
  # do other things you need to do in this loop

导致完整的字典副本,因此如果字典变大或经常调用包含函数,则存在愚蠢性能损失的风险。

更好的方法是仅复制密钥而不是整个字典:

for k in list(dict_.keys()):
  if condition(k, dict_[k]):
    del dict_[k]
    continue
  # do other things you need to do in this loop       

(请注意,所有代码示例都在Python 3中,因此keys()items()返回视图,而不是副本。)

在大多数情况下,它不会对性能造成太大影响,因为检查即使是最简单的条件(更不用说你在循环中做的其他事情)的时间通常要大于添加一个键的时间。一个清单。

尽管如此,我想知道是否有可能避免使用自定义字典在迭代时允许删除:

for k, v in dict_.items():
  if condition(k, v):
    del dict_[k]
    continue
  # do other things you need to do in this loop

也许迭代器总是可以向前看,所以当调用__next__时,迭代器甚至不知道当前元素就知道去哪里(它只需要在第一次获取元素时查看它它)。如果没有下一个元素,迭代器可以设置一个标志,只要再次调用StopIteration就会引发__next__异常。

如果迭代器尝试前进的元素被删除,则可以引发异常;在多次迭代同时进行时,不需要支持删除。

这种方法有问题吗?

一个问题是,与现有dict相比,我不确定是否可以完成没有重大开销;否则,使用list(dict_)方法会更快!

更新:

我尝试了所有版本。我没有报告时间,因为它们显然非常依赖于确切的情况。但似乎可以肯定地说,在许多情况下,最快的方法可能是list(dict_)。毕竟,如果你想一想,副本是最快的操作,它随着列表的大小线性增长;几乎任何其他开销,只要它也与列表大小成比例,可能会更大。

我非常喜欢所有的想法,但由于我必须只选择一个,我接受上下文管理器解决方案,因为它允许使用字典作为正常或“增强”,代码更改非常小。

7 个答案:

答案 0 :(得分:17)

如您所知,您可以将项目存储在某处删除,并推迟删除它们。然后问题变为清除它们时,如何以确保最终调用purge方法。答案是一个上下文管理器,它也是dict的子类。

class dd_dict(dict):    # the dd is for "deferred delete"
    _deletes = None
    def __delitem__(self, key):
        if key not in self:
            raise KeyError(str(key))
        dict.__delitem__(self, key) if self._deletes is None else self._deletes.add(key)
    def __enter__(self):
        self._deletes = set()
    def __exit__(self, type, value, tb):
        for key in self._deletes:
            try:
                dict.__delitem__(self, key)
            except KeyError:
                pass
        self._deletes = None

用法:

# make the dict and do whatever to it
ddd = dd_dict(a=1, b=2, c=3)

# now iterate over it, deferring deletes
with ddd:
    for k, v in ddd.iteritems():
        if k is "a":
            del ddd[k]
            print ddd     # shows that "a" is still there

print ddd                 # shows that "a" has been deleted

如果您不在with区块,当然,删除是立即的;因为这是一个dict子类,它就像上下文管理器之外的常规dict一样。

您也可以将其实现为字典的包装类:

class deferring_delete(object):
    def __init__(self, d):
        self._dict = d
    def __enter__(self):
        self._deletes = set()
        return self
    def __exit__(self, type, value, tb):
        for key in self._deletes:
            try:
                del self._dict[key]
            except KeyError:
                pass
        del self._deletes
    def __delitem__(self, key):
        if key not in self._dict:
            raise KeyError(str(key))
        self._deletes.add(key)

d = dict(a=1, b=2, c=3)

with deferring_delete(d) as dd:
    for k, v in d.iteritems():
        if k is "a":
            del dd[k]    # delete through wrapper

print d

如果你愿意的话,甚至可以使包装类完全像字典那样起作用,尽管那是更多的代码。

在性能方面,这无疑是不是一场胜利,但我喜欢从程序员友好的角度来看。第二种方法应该非常快一些,因为它没有在每次删除时测试一个标志。

答案 1 :(得分:8)

您需要做的是不修改迭代的键列表。您可以通过三种方式执行此操作:

  1. 在单独的列表中复制密钥并对其进行迭代。然后,您可以在迭代期间安全地删除字典中的键。这是最简单,最快速的,除非字典是 huge ,在这种情况下,您应该开始考虑在任何情况下使用数据库。代码:

    for k in list(dict_):
      if condition(k, dict_[k]):
        del dict_[k]
        continue
      # do other things you need to do in this loop
    
  2. 复制不是您要迭代的键,而是复制要删除的键。换句话说,在迭代时不要删除这些键而是将它们添加到列表中,然后在完成迭代后删除该列表中的键。这比1稍微复杂,但远小于3.它也很快。这就是你在第一个例子中所做的。

    delete_these = []
    for k in dict_:
      if condition(k, dict_[k]):
        delete_these.append(k)
        continue
      # do other things you need to do in this loop
    
    for k in delete_these:
        del dict_[k]
    
  3. 如你所说,避免制作某种新列表的唯一方法就是创建一个特殊字典。但是当你删除密钥时它实际上并没有删除密钥,但只将它们标记为已删除,然后只有在调用清除方法后才将它们删除。这需要相当多的实现,并且有边缘情况,你会通过忘记清除等来捏造自己。迭代字典必须仍然包括已删除的密钥,这将在某些时候咬你。所以我不推荐这个。 另外,无论你是在Python中实现这一点,你都可能再一次得到一个要删除的东西列表,所以它可能只是一个复杂且容易出错的版本2.如果你在C中实现它,你可以通过直接将标志添加到哈希键结构中来逃避复制。但如上所述,问题确实掩盖了这些好处。

答案 2 :(得分:4)

您可以通过遍历字典的键/值对的静态列表来完成此操作,而不是迭代字典视图。

基本上,迭代list(dict_.items())代替dict_.items()将起作用:

for k, v in list(dict_.items()):
  if condition(k, v):
    del dict_[k]
    continue
  # do other things you need to do in this loop

以下是一个示例(ideone):

dict_ = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g'}
for k, v in list(dict_.items()):
    if k % 2 == 0:
        print("Deleting  ", (k, v))
        del dict_[k]
        continue
    print("Processing", (k, v))

和输出:

Deleting   (0, 'a')
Processing (1, 'b')
Deleting   (2, 'c')
Processing (3, 'd')
Deleting   (4, 'e')
Processing (5, 'f')
Deleting   (6, 'g')

答案 3 :(得分:3)

Python 2.x和3.x的朴素实现:

import sys
from collections import deque


def _protect_from_delete(func):
    def wrapper(self, *args, **kwargs):
        try:
            self._iterating += 1
            for item in func(self, *args, **kwargs):
                yield item
        finally:
            self._iterating -= 1
            self._delete_pending()
    return wrapper

class DeletableDict(dict):
    def __init__(self, *args, **kwargs):
        super(DeletableDict, self).__init__(*args, **kwargs)
        self._keys_to_delete = deque()
        self._iterating = 0

    if sys.version_info[0] != 3:
        iterkeys = _protect_from_delete(dict.iterkeys)
        itervalues = _protect_from_delete(dict.itervalues)
        iteritems = _protect_from_delete(dict.iteritems)
    else:
        keys = _protect_from_delete(dict.keys)
        values = _protect_from_delete(dict.values)
        items = _protect_from_delete(dict.items)  
    __iter__ = _protect_from_delete(dict.__iter__)

    def __delitem__(self, key):
        if not self._iterating:
            return super(DeletableDict, self).__delitem__(key)
        self._keys_to_delete.append(key)

    def _delete_pending(self):
        for key in self._keys_to_delete:
            super(DeletableDict, self).__delitem__(key)
        self._keys_to_delete.clear()

if __name__ == '__main__':
    dct = DeletableDict((i, i*2) for i in range(15))
    if sys.version_info[0] != 3:
        for k, v in dct.iteritems():
            if k < 5:
                del dct[k]
        print(dct)
        for k in dct.iterkeys():
            if k > 8:
                del dct[k]
        print(dct)
        for k in dct:
            if k < 8:
                del dct[k]
        print(dct)
    else:
        for k, v in dct.items():
            if k < 5:
                del dct[k]
        print(dct)

当迭代键,项或值时,它会设置标记self._iterating。在__delitem__中,它检查删除项目的能力,并将密钥存储在临时队列中。在迭代结束时,它会删除所有挂起的密钥。

这是非常天真的实现,我不建议在生产代码中使用它。

修改

添加了对Python 3的支持以及@jsbueno 条评论的改进。

Python 3 run on Ideone.com

答案 4 :(得分:3)

Python 3.2在stdlib中有这样的dict:

#!/usr/bin/env python3
from collections import OrderedDict as odict

d = odict(zip(range(3), "abc"))
print(d)
for k in d:
    if k == 2:
       del d[k]
print(d)

输出

OrderedDict([(0, 'a'), (1, 'b'), (2, 'c')])
OrderedDict([(0, 'a'), (1, 'b')])

在链接列表上执行迭代,请参阅__iter__() method implementationThe deletion is safe (in Python 3.2)即使项目是弱引用。

答案 5 :(得分:0)

  1. 您可以在迭代开始时复制键列表(您不需要复制te值),然后迭代这些键(检查键是否存在)。如果有很多键,这效率很低。
  2. 您可以安排将第一个示例代码嵌入到类中。 __iter____delitem__以及其他特殊方法需要协作以在迭代发生时保留要删除的项目列表。当没有当前迭代时,__delitem__只能删除一个项目,但是当至少发生一次迭代时,它应该只将要删除的密钥添加到列表中。当最后一个活动迭代完成时,它应该实际删除的东西。如果有很多要删除的键,这会有点低效,当然,如果总是至少进行一次迭代,它会爆炸。

答案 6 :(得分:0)

这可以作为两个例子之间的妥协 - 两条线比第二条线长,但比第一条更短,更快。 Python 2:

dict_ = {k : random.randint(0, 40000) for k in range(0,200000)}

dict_remove = [k for k,v in dict_.iteritems() if v < 3000]
for k in dict_remove:
    del dict_[k]

分成一个函数,每次调用都会降低到一行(无论这个是否更具可读性):

def dict_remove(dict_, keys):
    for k in keys:
        del dict_[k]

dict_remove(dict_, [k for k,v in dict_.iteritems() if v < 3000])

无论代码存储在何处,您都必须在某处存储需要删除的密钥。唯一的方法就是使用生成器表达式,这会在你第一次删除键时爆炸。