custom dict that allows delete during iteration
根据Lennart Regebro的答案更新
假设您迭代一个字典,有时需要删除一个元素。以下是非常有效的:
1 2 3 4 5 6 7 8 | 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] |
这里唯一的开销是构建要删除的键列表;除非它比字典的大小大,否则这不是问题。然而,这种方法需要一些额外的编码,所以它不是很流行。
流行的听写理解方法:
1 2 3 | 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 |
号
结果得到完整的字典副本,因此如果字典变大或经常调用包含函数,就有可能出现愚蠢的性能问题。
一个更好的方法是只复制键而不是整个字典:
1 2 3 4 5 | 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中,因此
在大多数情况下,它不会对性能造成太大的影响,因为检查最简单条件(更不用说循环中正在做的其他事情)的时间通常比向列表中添加一个键的时间要长。
不过,我想知道是否有可能避免使用允许在迭代时删除的自定义字典:
1 2 3 4 5 | for k, v in dict_.items(): if condition(k, v): del dict_[k] continue # do other things you need to do in this loop |
。
也许迭代器可以一直向前看,这样当调用
如果迭代器试图前进的元素最终被删除,则可以引发异常;当同时进行多个迭代时,不需要支持删除。
这种方法有什么问题吗?
一个问题是,与现有的
更新:
我试过所有的版本。我不报告时间,因为他们显然非常依赖于具体情况。但可以肯定地说,在许多情况下,最快的方法可能是
我真的很喜欢所有的想法,但是因为我只需要选择一个,所以我接受上下文管理器解决方案,因为它允许使用字典作为普通的或"增强的",只需很小的代码更改。
如您所注意的,您可以将要删除的项目存储在某个地方,并将其删除推迟到稍后。然后问题就变成了何时清除它们以及如何确保最终调用了purge方法。对此的答案是上下文管理器,它也是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 |
用途:
1 2 3 4 5 6 7 8 9 10 11 | # 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 |
号
当然,如果您不在
您还可以将其实现为字典的包装类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 2 3 4 5 | for k in list(dict_): if condition(k, dict_[k]): del dict_[k] continue # do other things you need to do in this loop |
。
不要复制正在迭代的键,而是复制要删除的键。换句话说,不要在迭代时删除这些键,而是将它们添加到列表中,然后在迭代完成后删除该列表中的键。这比1稍微复杂一些。但远低于3。它也很快。这就是您在第一个示例中所做的。
1 2 3 4 5 6 7 8 9 | 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] |
如你所建议的,避免编出某种新列表的唯一方法就是编一本特别的字典。但这要求删除键时,它不会实际删除键,而是将它们标记为已删除,然后仅在调用purge方法后才真正删除它们。这需要大量的实现,并且存在一些边缘情况,您会忘记清除等,从而欺骗自己,并且迭代字典时必须仍然包含已删除的键,这在某些时候会咬到您。所以我不推荐这个。另外,不管您在Python中如何实现这一点,您可能会再次得到一个要删除的内容列表,因此它可能只是2的一个复杂且容易出错的版本。如果您在C中实现它,那么您可以通过将标志直接添加到哈希键结构中来避免复制。但如前所述,这些问题确实掩盖了好处。
您可以通过迭代字典的键/值对的静态列表来实现这一点,而不是迭代字典视图。
基本上,迭代
1 2 3 4 5 | 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):
1 2 3 4 5 6 7 | 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)) |
号
输出:
1 2 3 4 5 6 7 | Deleting (0, 'a') Processing (1, 'b') Deleting (2, 'c') Processing (3, 'd') Deleting (4, 'e') Processing (5, 'f') Deleting (6, 'g') |
python 3.2在stdlib中有这样的dict:
1 2 3 4 5 6 7 8 9 | #!/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) |
。产量
1 2 | OrderedDict([(0, 'a'), (1, 'b'), (2, 'c')]) OrderedDict([(0, 'a'), (1, 'b')]) |
迭代是在链表上执行的,参见
python 2.x和3.x的简单实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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) |
迭代键、项或值时,它设置标志
这是非常幼稚的实现,我不建议在生产代码中使用它。
编辑
增加了对python 3的支持和@jsbueno注释的改进。
python 3在ideone.com上运行
这可以作为两个示例之间的折衷方案——两条线比第二条线长,但比第一条线短且略快。Python2:
1 2 3 4 5 | 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] |
。
分成一个函数,每一个调用都只剩下一行(无论您的调用是否可读):
1 2 3 4 5 | 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]) |
。
无论代码存储在何处,您都必须将需要删除的密钥存储在某个地方。唯一的解决方法是使用生成器表达式,它将在您第一次删除键时爆炸。