2015-10-16 90 views
2

我正在创建一个经典的“set”类来练习,我想要做的第一件事是删除所有重复项。我知道我可以用字典键很容易地做到这一点,但我想尝试改进我的列表理解。这两个功能应该做同样的事情,但第二个功能不起作用。为什么?清单列表理解中的remove()方法表达式错误

for element in elements: 
      if elements.count(element) > 1: 
       elements.remove(element) 
     print(elements) 

第二:

self.elements = [elements.remove(element) for element in elements 
       if elements.count(element) > 1] 
+5

你的代码的任何版本都不会做你想要的。在迭代列表时突变列表将跳过一些值! – Blckknght

+0

[在Python中迭代时从列表中删除项目]可能的重复(http://stackoverflow.com/questions/1207406/remove-items-from-a-list-while-iterating-in-python) – Makoto

+0

list of list (集(元素))? – rebeling

回答

4

不要过度迭代,并从相同的列表中删除,你也应该使用Counter字典,如果你的对象是可哈希计算每个元素的出现:

from collections import Counter 
cn = Counter(elements) 
# elements[:] changes original list 
elements[:] = (ele for ele in elements if ch[ele] < 2) 

在你的第二个代码,因为list.remove就地操作它只会增加None's随时if elements.count(element) > 1True否则什么也不做,所以这两个代码示例是完全不同的。

第一个代码,如果它的工作只是偶然的。当你从你的列表中删除一个元素时,以前指向的指针可以改变,所以你最终从列表中删除错误的元素。

的你的第二个代码做什么,以及为什么你的第一个是错误的做法的一个例子:

In [20]: l = [2,3,1,4,1,5] 

In [21]: l = [l.remove(i) if i > 1 else i for i in l] 

In [22]: l 
Out[22]: [None, 1, None, None] 

因为你已经改变了你最终的指针值去除第二1,并与一些无真实添加,因为像所有在位操作的函数或者在python中没有指定返回值,它们默认返回None。

如果你真的想获得一个独特的集合中的所有元素,而不仅仅是保持独特的元素是你的代码似乎有什么要尝试并维持秩序,一个collections.OrderedDict字典会做你需要的东西:

from collections import OrderedDict 
elements[:] = collections.OrderedDict.fromkeys(elements) 
+0

谢谢,这是有道理的! – flybonzai

+0

没有概率,Counter dict方法也使得你的代码'O(n)'与'O(n^2)'相对,因为我们只做一个单独的过程来获得计数,然后再传递一次来过滤原始列表 –

+0

'计数器'代码不会做提问者想要的。它消除了重复值的所有副本,而不是除了一个之外的所有副本。 – Blckknght

1

您的代码有两个问题。第一个问题是你明确询问的问题:列表理解版本将为self.elements分配一大堆None值。 None值只是您拨打list.remove时的返回值。它修改了列表,并没有任何有用的返回(因此它返回None)。

理解[element for element in elements if elements.count(element) == 1 or elements.remove(element)]将与您的其他代码一样工作(因为None是falsey和or短路),但它仍然会遇到第二个问题。 (这也是一个丑陋的黑客:由理解创建的新列表将具有elements相同的内容,因为remove修改为elements就位,这是相当混乱。编写难以理解的代码通常不是一个好主意。)

第二个问题是在迭代它时修改列表可能会导致问题。列表迭代器按索引工作。迭代器产生的第一个项目来自索引0,第二个来自索引1,依此类推。如果您通过在列表的早期删除项目来修改列表,则会移动所有后续项目的索引。

因此,假设您在您的迭代器向您显示它之后删除第一项(从索引0)。这个列表将会把所有后面的值都转移,但迭代器不会知道这个。它仍然会在下一个索引1处产生该项目,即使该索引曾经是索引2处的项目(在该列表被修改之前)。最初在索引1处(并且在前一个项目之后的索引0处被移除)的项目将被迭代跳过。

这里的此问题,其中值2,5的一个简单的例子,和图8将不被打印:

L = list(range(10)) # [0,1,2,3,4,5,6,7,8,9] 
for x in L: 
    print(x) 
    if x % 3 == 1: # true for 1,4, and 7 
     L.remove(x) 

在该示例中,用于去除值逻辑是非常简单的,我们从未跳过一我们通常希望删除的值(因此L的末尾预期值为[0,2,3,5,6,8,9]),但其他代码可能无法正常工作。

避免此问题的一种方法是在修改原始文件时迭代列表副本。在这种情况下,我们还需要count原,而不是副本:

for element in elements[:]: # copy list with a slice here! 
    if elements.count(element) > 1: 
     elements.remove(element) # modify the original list 

这是相当低效虽然,因为从列表中删除的项目(在端部以外的位置)需要花时间把所有后来的数值上移一个位置。计数也很慢,因为您需要迭代每个项目的整个列表。这是更有效的跟踪到目前为止你见过的唯一项目,并跳过重复的项目,当你以后看到他们:

seen = set() 
results = [] 
for element in elements: 
    if element not in seen: 
     seen.add(element) 
     results.append(element) 

你甚至可以建立一个有点尴尬的列表理解(副作用)的此代码:

seen = set() 
results = [element for element in elements 
      if not (element in seen or seen.add(element))] 

一种更好的方法是通常的重复数据删除逻辑捆绑成发电机功能(如itertools文档中的unique_everseen recipe),然后用list(dedupe(elements))调用它。