Elegant way to remove items from sequence in Python?
当我用python编写代码时,我经常需要根据某些条件从列表或其他序列类型中删除项。我还没有找到一个优雅高效的解决方案,因为从当前正在迭代的列表中删除项目是不好的。例如,您不能这样做:
1 2 3 | for name in names: if name[-5:] == 'Smith': names.remove(name) |
我通常会这样做:
1 2 3 4 5 6 7 | toremove = [] for name in names: if name[-5:] == 'Smith': toremove.append(name) for name in toremove: names.remove(name) del toremove |
这是内在的,相当难看,而且可能有问题(它如何处理多个"john smith"条目?).是否有人有更优雅的解决方案,或者至少有更高效的解决方案?
用字典的怎么样?
实现过滤的两种简单方法是:
使用
使用列表理解:
注意,这两种情况都保留谓词函数计算为
编辑搞笑…两个人分别张贴了我在张贴我的答案时提出的两个答案。
您还可以在列表上向后迭代:
1 2 3 | for name in reversed(names): if name[-5:] == 'Smith': names.remove(name) |
这样做的好处是,它不创建新的列表(如
请注意,尽管在向后迭代时删除元素是安全的,但是插入它们是比较棘手的。
显而易见的答案是约翰和其他几个人给出的答案,即:
1 | >>> names = [name for name in names if name[-5:] !="Smith"] # <-- slower |
但这样做的缺点是,它创建了一个新的列表对象,而不是重用原始对象。我做了一些分析和实验,我想到的最有效的方法是:
1 | >>> names[:] = (name for name in names if name[-5:] !="Smith") # <-- faster |
分配给"名称[:]"基本上意味着"用以下值替换名称列表的内容"。它不同于只分配给名称,因为它不会创建新的列表对象。赋值的右边是一个生成器表达式(注意使用括号而不是方括号)。这将导致python在列表中迭代。
一些快速分析表明,这比列表理解方法快30%,比过滤方法快40%。
注意:虽然这个解决方案比明显的解决方案更快,但它更模糊,并且依赖于更高级的Python技术。如果您确实使用它,我建议您在使用时附带一条评论。它可能只在您真正关心这个特定操作的性能的情况下才值得使用(无论什么情况下都非常快)。(在我使用它的情况下,我进行了一次*波束搜索,并使用它从搜索波束中删除搜索点。)
使用列表理解
1 | list = [x for x in list if x[-5:] !="smith"] |
有时筛选(使用筛选或列表理解)不起作用。当其他对象持有对您正在修改的列表的引用,并且需要在适当的位置修改该列表时,就会发生这种情况。
1 2 3 | for name in names[:]: if name[-5:] == 'Smith': names.remove(name) |
与原始代码的唯一区别是在for循环中使用了
这个过滤器太棒了。简单例子:
1 2 3 | names = ['mike', 'dave', 'jim'] filter(lambda x: x != 'mike', names) ['dave', 'jim'] |
编辑:科里的列表理解能力也很棒。
如果应该就地过滤列表,并且列表大小相当大,那么前面的答案中提到的基于list.remove()的算法可能不合适,因为它们的计算复杂性是o(n^2)。在这种情况下,您可以使用以下no so python函数:
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 | def filter_inplace(func, original_list): """ Filters the original_list in-place. Removes elements from the original_list for which func() returns False. Algrithm's computational complexity is O(N), where N is the size of the original_list. """ # Compact the list in-place. new_list_size = 0 for item in original_list: if func(item): original_list[new_list_size] = item new_list_size += 1 # Remove trailing items from the list. tail_size = len(original_list) - new_list_size while tail_size: original_list.pop() tail_size -= 1 a = [1, 2, 3, 4, 5, 6, 7] # Remove even numbers from a in-place. filter_inplace(lambda x: x & 1, a) # Prints [1, 3, 5, 7] print a |
编辑:事实上,在https://stackoverflow.com/a/4639748/274937上的解决方案优于矿山解决方案。它更像Python,工作速度更快。因此,下面是一个新的filter inplace()实现:
1 2 3 4 5 6 7 8 9 | def filter_inplace(func, original_list): """ Filters the original_list inplace. Removes elements from the original_list for which function returns False. Algrithm's computational complexity is O(N), where N is the size of the original_list. """ original_list[:] = [item for item in original_list if func(item)] |
要回答有关使用字典的问题,您应该注意,python3.0将包含dict理解:
1 | >>> {i : chr(65+i) for i in range(4)} |
同时,你可以这样做一个准听写理解:
1 | >>> dict([(i, chr(65+i)) for i in range(4)]) |
或者作为一个更直接的答案:
1 | dict([(key, name) for key, name in some_dictionary.iteritems if name[-5:] != 'Smith']) |
解决方案、过滤和理解都需要构建一个新的列表。我不太清楚Python的内部结构,但我认为更传统(但不太优雅)的方法可能更有效:
1 2 3 4 5 6 7 8 9 10 11 | names = ['Jones', 'Vai', 'Smith', 'Perez'] item = 0 while item <> len(names): name = names [item] if name=='Smith': names.remove(name) else: item += 1 print names |
总之,对于简短的列表,我坚持前面提出的两个解决方案中的任何一个。
1 | names = filter(lambda x: x[-5:] !="Smith", names); |
这里是我的
1 2 3 4 5 6 7 8 9 10 11 12 | def filter_inplace(conditionFunc, list, reversed=False): index = 0 while index < len(list): item = list[index] shouldRemove = not conditionFunc(item) if reversed: shouldRemove = not shouldRemove if shouldRemove: list.remove(item) else: index += 1 |
如果是一套。
1 2 3 4 5 | toRemove = set([]) for item in mySet: if item is unwelcome: toRemove.add(item) mySets = mySet - toRemove |
对于您的示例,过滤器和列表理解是可以的,但它们有几个问题:
- 他们会复制你的列表并返回新的列表,当原来的列表很大时,效率会很低。
- 当选择项目的条件(在您的例子中,如果name[-5:]='smith')更复杂或有多个条件时,它们可能真的很麻烦。
您的原始解决方案实际上对非常大的列表更有效,即使我们同意它更丑。但是,如果您担心可以有多个"john smith",可以根据位置而不是根据值删除:
1 2 3 4 5 6 7 8 9 10 | names = ['Jones', 'Vai', 'Smith', 'Perez', 'Smith'] toremove = [] for pos, name in enumerate(names): if name[-5:] == 'Smith': toremove.append(pos) for pos in sorted(toremove, reverse=True): del(names[pos]) print names |
我们不能在不考虑列表大小的情况下选择解决方案,但是对于大列表,我更喜欢您的2通解决方案,而不是筛选或列表理解
很明显,这是您使用的数据结构的问题。例如,使用哈希表。有些实现支持每个键有多个条目,因此可以关闭最新的元素,或者删除所有的元素。
但这是,而且您将要找到的解决方案是,通过不同的数据结构实现优雅,而不是算法。也许你可以做得更好,如果它是排序的,或者什么的,但是列表上的迭代是你这里唯一的方法。
编辑:有人意识到他要求"效率"…所有这些建议的方法只是对列表进行迭代,这与他建议的方法相同。