Python列表元素的删除

4
我有两个列表,
l1 = [1,2,3,4,5,6]
l2 = [3,2]

我希望你能够移除列表l1中在列表l2中出现的元素。为此,我已经尝试了以下代码:
for x in l1:
    if x in l2:
        l1.remove(x)

它会输出类似的结果:
[1, 3, 4, 5, 6]

但输出应该像这样
[1, 4, 5, 6]

有人能够解释一下这个问题吗?

9个回答

9
这可以很容易地解释如下。
考虑您拥有的第一个数组:
| 1 | 2 | 3 | 4 | 5 | 6 |

现在你开始迭代。
| 1 | 2 | 3 | 4 | 5 | 6 |
  ^

什么都没有发生,迭代器增加。
| 1 | 2 | 3 | 4 | 5 | 6 |
      ^

"2被移除了"
| 1 | 3 | 4 | 5 | 6 |
      ^

迭代器增量
| 1 | 3 | 4 | 5 | 6 |
          ^

翻译:

瞧,数字3还在。

解决方法是迭代一个向量的副本,例如:

for x in l1[:]: <- slice on entire array
    if x in l2:
        l1.remove(x)

或者反向迭代:
for x in reversed(l1):
    if x in l2:
        l1.remove(x)

这个句子的意思是:“表现出这种行为的东西是什么:”。
| 1 | 2 | 3 | 4 | 5 | 6 |
              ^

| 1 | 2 | 3 | 4 | 5 | 6 |
          ^

| 1 | 2 | 4 | 5 | 6 |
          ^

| 1 | 2 | 4 | 5 | 6 |
      ^

| 1 | 4 | 5 | 6 |
      ^

| 1 | 4 | 5 | 6 |
  ^

7
为什么不让它再简单一些呢?如果我们只想删除在l2中存在的元素,就没有必要实际迭代l1了。
for item in l2:
    while item in l1:
        l1.remove(item)

这将为您提供所需的输出...

另外,正如评论者所指出的,如果有可能出现重复:

l1 = filter(lambda x: x not in l2, l1)

或使用列表推导等许多其他变体。


@TimPietzcker 也许把 if 改成 while 更好? :) - poke
@TimPietzcker 绝对正确 - 没有考虑到那种情况。 - petr
@TimPietzcker 很好的发现,我会去修改我的答案来解决这个问题。 - millimoose
1
@poke的想法将if替换为while是非常正确的。现在你的解决方案是迄今为止最快的。 - Tim Pietzcker

3
您希望外层循环是这样读取的:
for x in l1[:]:
   ...

您不能在迭代列表时更改它并期望得到合理的结果。上面的技巧会复制l1并遍历副本。
请注意,如果输出列表中的顺序无关紧要,并且您的元素是唯一且可哈希的,则可以使用set:
set(l1).difference(l2)

这将给你一个集合作为输出,但你可以轻松地从中构建一个列表:

l1 = list(set(l1).difference(l2))

集合被严重低估了。 ;) - K.-Michael Aye

2

正如其他人所说的,你不能在循环列表时编辑它。这里一个好的选择是使用列表推导式来创建一个新列表。

removals = set(l2)
l1 = [item for item in l1 if item not in removals]

我们使用集合作为成员检查的数据结构,因为它比列表更快。


1
你是否曾经进行过基准测试,以测试成员资格检查在何时对于集合比列表更快?我预计对于长度为2的集合,“in”操作在列表上会更快(平均而言)比在集合上。但是对于长度为4的集合,我猜测“set”会更快。我不太确定,我经常想设置一个基准测试来检查,但我从未这样做过。 - mgilson
@millimoose -- 你当然是对的,这只是一个微小的优化,不值得担心。这只是我好奇而已(我同意,除非你知道集合的大小,否则一定要使用set)。 - mgilson
1
如果你在左边使用切片赋值,你也可以将其“原地”修改:l1[:] = [item for item in l1 ... ] - mgilson
但是,既然集合允许减法,为什么要使用列表推导呢? - K.-Michael Aye
@K.-MichaelAye 因为将l1设置为集合会导致失去顺序和重复项。如果可以接受,那么集合减法确实是最优的选择。 - Gareth Latty
显示剩余4条评论

2
如果l1中的顺序和重复项的丢失不重要:
list(set(l1) - set(l2))

如果你需要将结果作为列表,最后一个list()是必需的。你也可以直接使用结果集,它也是可迭代的。 如果你需要排序,你当然可以在结果列表上调用l.sort()。


1

编辑:我删除了我的原始答案,因为即使它给出了正确的结果,但是它的原因并不直观,并且速度也不够快...所以我只留下了时间:

import timeit

setup = """l1 = list(range(20)) + list(range(20))
l2 = [2, 3]"""

stmts = {
"mgilson": """for x in l1[:]:
    if x in l2:
        l1.remove(x)""",
"petr": """for item in l2:
    while item in l1:
        l1.remove(item)""",
"Lattyware": """removals = set(l2)
l1 = [item for item in l1 if item not in removals]""",
"millimoose": """for x in l2:
    try: 
        while True: l1.remove(x)
    except ValueError: pass""",
"Latty_mgilson": """removals = set(l2)
l1[:] = (item for item in l1 if item not in removals)""",
"mgilson_set": """l1 = list(set(l1).difference(l2))"""
}        

for idea in stmts:
    print("{0}: {1}".format(idea, timeit.timeit(setup=setup, stmt=stmts[idea])))

结果(Python 3.3.0 64位,Win7):

mgilson_set: 2.5841989922197333
mgilson: 3.7747968857414813
petr: 1.9669433777815701
Latty_mgilson: 7.262900152285258
millimoose: 3.1890831105541793
Lattyware: 4.573971325181478

我不确定依赖迭代器的实现细节是否让我感到舒适。或者在反向迭代时,list 是否明确保证支持并发修改? - millimoose
@millimoose:是的,至少在这种情况下是这样的,因为如果我们正在删除某些东西,它总是我们已经迭代过的一个项。 - Tim Pietzcker
1
@Tim,小心,第二个例子中,当您迭代第二个例子时,您正在删除第一个“2”。 - John La Rooy
@gnibbler:可能吧,但我觉得这很不安。我已经删除了我的答案,只留下了时间。 - Tim Pietzcker
2
其实,我现在明白了。如果你删除一个在迭代器当前位置之前的重复项,那么删除操作将把当前元素向左移动一个位置,下一次迭代将再次遇到它,直到最后被删除。不过,我仍然不确定这是否使得方法完全正确。(我认为我会坚持我的保留意见,不依赖于迭代器状态在面对共同修改时做正确的事情 - 这是一个可怕的潜在陷阱,即使要做像这样微不足道的事情也要记住。) - millimoose
显示剩余13条评论

0
l1 = [1, 2, 3, 4, 5, 6]
l2 = [3, 2]
[l1.remove(x) for x in l2]
print l1
[1, 4, 5, 6]

0

在迭代列表l1时修改它,这会导致奇怪的行为。(在迭代过程中3将被跳过。)

要么迭代副本,要么改变算法以迭代l2

for x in l2:
    try: 
        while True: l1.remove(x)
    except ValueError: pass

(这应该比显式测试if x in l1更好。)不,当l1增长时,这个表现非常糟糕。


0

就我所知,使用更现实的输入数据集并采用稍微严格一些(但其他方面相同)的方法来计时不同人的答案,我的结果与@Tim Pietzcker的结果有显著不同。

名称和代码片段与Tim的相同,只是我添加了一个名为Lattyware的变体,称为Lattyware_rev,它确定要保留而不是拒绝的元素--结果比前者慢。 请注意,最快的两个不保留l1的顺序。

以下是最新的计时代码:

import timeit

setup = """
import random
random.seed(42) # initialize to constant to get same test values
l1 = [random.randrange(100) for _ in xrange(100)]
l2 = [random.randrange(100) for _ in xrange(10)]
"""

stmts = {
"Minion91": """
for x in reversed(l1):
    if x in l2:
        l1.remove(x)
""",

"mgilson": """
for x in l1[:]: # correction
    if x in l2:
        l1.remove(x)
""",
"mgilson_set": """
l1 = list(set(l1).difference(l2))
""",

"Lattyware": """
removals = set(l2)
l1 = [item for item in l1 if item not in removals]
""",
"Lattyware_rev": """
keep = set(l1).difference(l2)
l1 = [item for item in l1 if item in keep]
""",
"Latty_mgilson": """
removals = set(l2)
l1[:] = (item for item in l1 if item not in removals)""",

"petr": """
for item in l2:
    while item in l1:
        l1.remove(item)
""",
"petr (handles dups)": """
l1 = filter(lambda x: x not in l2, l1)
""",

"millimoose": """
for x in l2:
    try:
        while True: l1.remove(x)
    except ValueError: pass
""",

"K.-Michael Aye": """
l1 = list(set(l1) - set(l2))
""",

}

N = 10000
R = 3

timings = [(idea,
            min(timeit.repeat(stmts[idea], setup=setup, repeat=R, number=N)),
           ) for idea in stmts]
longest = max(len(t[0]) for t in timings)  # length of longest name
exec(setup)  # get an l1 & l2 just for heading length measurements
print('fastest to slowest timings of ideas:\n' +\
      '  ({:,d} timeit calls, best of {:d} executions)\n'.format(N, R)+\
      '   len(l1): {:,d}, len(l2): {:,d})\n'.format(len(l1), len(l2)))
for i in sorted(timings, key=lambda x: x[1]):  # sort by speed (fastest first)
    print "{:>{width}}:  {}".format(*i, width=longest)

输出:

fastest to slowest timings of ideas:
  (10,000 timeit calls, best of 3 executions)
   len(l1): 100, len(l2): 10)

        mgilson_set:  0.143126456832
     K.-Michael Aye:  0.213544010551
          Lattyware:  0.23666971551
      Lattyware_rev:  0.466918513924
      Latty_mgilson:  0.547516608553
               petr:  0.552547776807
            mgilson:  0.614238139366
           Minion91:  0.728920176815
         millimoose:  0.883061820848
petr (handles dups):  0.984093136969

当然,如果有什么根本性的错误可以解释这些截然不同的结果,请告诉我。


我知道这个帖子有点老了,所以请原谅我挖坟。但是我注意到Lattyware_rev测试有问题 - 对l1的赋值创建了一个生成器,但是没有消耗它,所以它实际上并没有做和其他测试相同的事情。 - Gareth Latty
1
@Lattyware:发现得不错... 不过修复它并没有什么太大的区别。无论如何,需要更新信息。谢谢。 - martineau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接