根据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_)
。毕竟,如果你想一想,副本是最快的操作,它随着列表的大小线性增长;几乎任何其他开销,只要它也与列表大小成比例,可能会更大。
我非常喜欢所有的想法,但由于我必须只选择一个,我接受上下文管理器解决方案,因为它允许使用字典作为正常或“增强”,代码更改非常小。
答案 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)
您需要做的是不修改迭代的键列表。您可以通过三种方式执行此操作:
在单独的列表中复制密钥并对其进行迭代。然后,您可以在迭代期间安全地删除字典中的键。这是最简单,最快速的,除非字典是 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
复制不是您要迭代的键,而是复制要删除的键。换句话说,在迭代时不要删除这些键而是将它们添加到列表中,然后在完成迭代后删除该列表中的键。这比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]
如你所说,避免制作某种新列表的唯一方法就是创建一个特殊字典。但是当你删除密钥时它实际上并没有删除密钥,但只将它们标记为已删除,然后只有在调用清除方法后才将它们删除。这需要相当多的实现,并且有边缘情况,你会通过忘记清除等来捏造自己。迭代字典必须仍然包括已删除的密钥,这将在某些时候咬你。所以我不推荐这个。 另外,无论你是在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 条评论的改进。
答案 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 implementation。 The deletion is safe (in Python 3.2)即使项目是弱引用。
答案 5 :(得分:0)
__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])
无论代码存储在何处,您都必须在某处存储需要删除的密钥。唯一的方法就是使用生成器表达式,这会在你第一次删除键时爆炸。