Python - 从列表中删除项目

oro*_*aki 9 python list-comprehension

# I have 3 lists:
L1 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
L2 = [4, 7, 8]
L3 = [5, 2, 9]
# I want to create another that is L1 minus L2's memebers and L3's memebers, so:
L4 = (L1 - L2) - L3  # Of course this isn't going to work
Run Code Online (Sandbox Code Playgroud)

我想知道,做到这一点的"正确"方法是什么.我可以用很多不同的方式来做,但Python的风格指南说应该只有一种正确的方法来做每件事.我从来不知道这是什么.

Bra*_*des 10

以下是一些尝试:

L4 = [ n for n in L1 if (n not in L2) and (n not in L3) ]  # parens for clarity

tmpset = set( L2 + L3 )
L4 = [ n for n in L1 if n not in tmpset ]
Run Code Online (Sandbox Code Playgroud)

现在我有一点时间思考,我意识到这L2 + L3件事会创建一个临时列表,立即被抛弃.所以更好的方法是:

tmpset = set(L2)
tmpset.update(L3)
L4 = [ n for n in L1 if n not in tmpset ]
Run Code Online (Sandbox Code Playgroud)

更新:我看到一些关于性能的奢侈声明,我想声称我的解决方案已经尽可能快.创建中间结果,无论它们是中间列表还是必须被重复调用的中间迭代器,总是会比简单地给出L2L3直接迭代一样慢,就像我在这里做的那样.

$ python -m timeit \
  -s 'L1=range(300);L2=range(30,70,2);L3=range(120,220,2)' \
  'ts = set(L2); ts.update(L3); L4 = [ n for n in L1 if n not in ts ]'
10000 loops, best of 3: 39.7 usec per loop
Run Code Online (Sandbox Code Playgroud)

所有其他选择(我能想到)都必然比这慢.例如,自己做循环,而不是让set()构造函数执行它们,增加了费用:

$ python -m timeit \
  -s 'L1=range(300);L2=range(30,70,2);L3=range(120,220,2)' \
  'unwanted = frozenset(item for lst in (L2, L3) for item in lst); L4 = [ n for n in L1 if n not in unwanted ]'
10000 loops, best of 3: 46.4 usec per loop
Run Code Online (Sandbox Code Playgroud)

使用迭代器,它们涉及的所有状态保存和回调显然会更加昂贵:

$ python -m timeit \
  -s 'L1=range(300);L2=range(30,70,2);L3=range(120,220,2);from itertools import ifilterfalse, chain' \
  'L4 = list(ifilterfalse(frozenset(chain(L2, L3)).__contains__, L1))' 
10000 loops, best of 3: 47.1 usec per loop
Run Code Online (Sandbox Code Playgroud)

所以我相信我昨晚给出的答案仍然是遥远的(对于"遥远"的值大于5微秒,显然)是最好的,除非提问者有重复L1并希望每次删除一次副本出现在其他列表中.


int*_*ted 6

update ::: post包含对与frozensets相比较低的集合性能的错误指控的引用.我认为在这个实例中使用冻结集仍然是明智的,即使不需要对集合本身进行散列,只是因为它在语义上更正确.虽然,在实践中,我可能不会打扰额外的6个字符.我没有动力去编辑帖子,所以请注意"指控"链接链接到一些错误运行的测试.评论中记录了血淋淋的细节.:::更新

Brandon Craig Rhodes 发布的第二大块代码相当不错,但由于他没有回应我关于使用冷冻装置的建议(好吧,不是我开始写这篇文章的时候,无论如何),我将继续发布我自己.

手头工作的整个基础是检查一系列值(L1)中的每一个是否属于另一组值; 该组值是内容L2L3.在这句话中使用"set"这个词就说明了:即使L2并且L3lists,我们并不真正关心它们的类似列表的属性,例如它们的值所在的顺序或它们包含的每个属性的数量.我们只关心他们共同拥有的价值(在那里).

如果将该组值存储为列表,则必须逐个检查列表元素,并检查每个元素.这是相对耗时的,而且它的语义很糟糕:再次,它是一组"值",而不是列表.因此Python具有这些整齐的集合类型,它们包含许多独特的值,并且可以快速告诉您是否存在某些值.这与python的dict类型在查找键时的工作方式非常相似.

集合frozensets之间的区别在于集合是可变的,这意味着它们可以在创建后进行修改.这两种类型的文档都在这里.

由于我们需要创建的集合,存储在L2和中的值的并集L3一旦创建就不会被修改,它在语义上适合使用不可变数据类型.据称这也有一些性能上的好处.嗯,它有一些优势是有意义的; 否则,为什么Python会frozenset作为内置?

更新 ......

布兰登回答了这个问题:冻结套装的真正优势在于它们的不变性使它们可以清洗,允许它们成为字典键或其他套装的成员.

我运行了一些非正式的时序测试,比较了相对较大(3000元素)的冻结和可变集合的创建和查找速度; 差别不大.这与上述链接相冲突,但支持Brandon所说的关于它们相同但在可变性方面的内容.

...... 更新

现在,因为frozensets是不可变的,所以它们没有更新方法.Brandon使用该set.update方法避免创建然后丢弃临时列表以设置创建; 我将采取不同的方法.

items = (item for lst in (L2, L3) for item in lst)
Run Code Online (Sandbox Code Playgroud)

这个生成器表达式使items迭代器连续地遍历L2和的内容L3.不仅如此,它还没有创建一个完整的列表 - 中间对象.for在生成器中使用嵌套表达式有点令人困惑,但我设法通过记住它们按照与编写实际for循环时相同的顺序进行整理,例如,

def get_items(lists):
    for lst in lists:
        for item in lst:
            yield item
Run Code Online (Sandbox Code Playgroud)

生成器函数等效于我们分配给的生成器表达式items.好吧,除了它是一个参数化的函数定义,而不是直接赋值给变量.

无论如何,足够的题外话.发电机的重要性在于它们实际上并没有做任何事情.好吧,至少不是马上:他们只是设置工作,以便在迭代生成器表达式后再完成.这被正式称为懒惰.我们将通过传递itemsfrozenset函数来做到这一点(好吧,无论如何),迭代它并返回一个冷冻冷冻集.

unwanted = frozenset(items)
Run Code Online (Sandbox Code Playgroud)

实际上,您可以通过将生成器表达式放在调用内部来实际组合最后两行frozenset:

unwanted = frozenset(item for lst in (L2, L3) for item in lst)
Run Code Online (Sandbox Code Playgroud)

只要生成器表达式创建的迭代器是您正在调用的函数的唯一参数,这种简洁的语法技巧就可以工作.否则你必须在通常单独的括号中写它,就像你将一个元组作为参数传递给函数一样.

现在我们可以像Brandon一样建立一个新列表,并具有列表理解能力.它们使用与生成器表达式相同的语法,并且基本上做同样的事情,除了它们渴望而不是懒惰(再次,这些是实际的技术术语),因此他们可以正确地迭代项目并从中创建列表.

L4 = [item for item in L1 if item not in unwanted]
Run Code Online (Sandbox Code Playgroud)

这相当于将生成器表达式传递给list,例如

L4 = list(item for item in L1 if item not in unwanted)
Run Code Online (Sandbox Code Playgroud)

但更惯用.

因此,这将创建列表L4,其中包含的元素L1不在其中,L2或者L3维护它们最初的顺序以及它们的数量.


如果您只是想知道哪些值在L1但不在L2或中L3,则更容易:您只需创建该集合:

L1_unique_values = set(L1) - unwanted
Run Code Online (Sandbox Code Playgroud)

你可以用它来制作一个列表,就像st0le一样,但这可能不是你想要的.如果你真的想要只找到一L1,你可能有一个很好的理由将该保持为a set或者确实是frozenset:

L1_unique_values = frozenset(L1) - unwanted
Run Code Online (Sandbox Code Playgroud)

...... Annnnd,现在完全不同了:

from itertools import ifilterfalse, chain
L4 = list(ifilterfalse(frozenset(chain(L2, L3)).__contains__, L1))
Run Code Online (Sandbox Code Playgroud)

  • 不,冻结集合*不比普通集合更有效,它们只是"修改位未设置",因此您可以将它们用作字典键.换句话说,它们是可以使用的,这对于你在Python中可以做什么和不能做什么都是非常重要的.(它们也可以是其他组合的成员.)这也意味着你可以将它们传递给其他例程而不用担心它们会被修改,但是可靠性是一个更大的交易.但是,正如你的答案所说,创造它们很尴尬.所以我认为你的解决方案过于复杂而没有"胜利". (3认同)