【问题标题】:Python list comprehension- "pop" result from original list?Python列表理解-原始列表的“弹出”结果?
【发布时间】:2015-03-10 13:32:41
【问题描述】:

假设我在 Python 3.X 中有一个列表。我使用列表推导返回该列表的一个子集——是否有一种简单/Pythonic 的方式可以将该子集从原始列表中“弹出”(因此该子集中的元素在返回后不再位于原始列表中) ?谢谢!

示例(我在定义 my_func 时需要帮助):

a = [1, 2, 2, 3, 4, 5]
a, b = my_func(a, *kwargs)

然后我想要:

a = [1, 2, 2]
b = [3, 4, 5]

注意:我不想一次弹出一个值,而是一次弹出整个子集。 “流行”可能不是最好的术语,但我不知道是什么。

我能想到的最好方法是:

 temp = [idx for idx, val in enumerate(a) if val > 2]  
 b = [a[i] for i in temp]  
 a = [val for idx,val in enumerate(a) if idx not in temp]  

显然,我更喜欢更优雅、更高效的东西。 我想避免重复列表两次。

编辑:我认为这个问题是独一无二的,因为我不仅仅关心拆分列表 - 我想修改原始列表。将其中一个拆分拆分并分配给原始列表变量是一种可能的解决方案,但我希望可能有一种方法可以在不显式分配给原始列表变量的情况下做到这一点(类似于 b.append(a.pop( )))

【问题讨论】:

  • 添加示例代码,输入和输出
  • 如果list 中的数据实际上适用于set,您可以使用intersectiondifference 等运算符。
  • 流行音乐是什么意思?您是想再次抓取该子集还是要删除它?
  • 听起来他想要两个子集 - 新列表中过滤的一个和旧列表中未过滤的。

标签: python list-comprehension


【解决方案1】:

单行解决方案(请不要实际使用这个):

a = [7,4,2, 1, 5 , 11, 2]
[x for x in (a.pop() for i in range(len(a))) if (lambda x: True if x> 2 \
else a.insert(0, x))(x)]

更合理的解决方案

一般来说 - 如果您在弹出之前检查一个值是什么 - 这是在索引处读取,然后是弹出(更不用说跟踪迭代长度变化的数组的乐趣了),这似乎表明没有两个新列表没有实际意义。

所以我只会添加到两个新列表之一,即

a = [1, 2, 2, 3, 4, 5]
b=[]; c=[];
for i in range(len(a)): 
    if x > 2: b.append(x) 
    else: c.append(x) 

当然,从技术上讲,您可以从 a 弹出并插入回 a 您会使用 a.insert(0, x) 而不是 c.append(x) - 但操作循环遍历的列表通常是个坏主意。

另一种选择是排序列表和二等分,即

a = sorted(a) 
ind = bisect.bisect(a, 2) 
#Now both lists are here 
a, b= a[:ind], a[ind:]

哪种方法更可取实际上取决于您之后打算如何处理这些列表。

【讨论】:

  • 目前为止我最喜欢这个。出于我的目的,我需要多一行来在最后用 c 替换 a。如果没有人想出一些更简单的语法,我会选择这个。
  • 不要认为第二个适用于我的具有重复值的情况(我编辑了我的原始问题,错误地将我的列表称为集合;实际上,它具有重复值),但这仍然是一个有趣的问题技术。
  • 二等分有左右二等分,所以你可以指定在重复值的情况下应该在哪里平分。通常,如果我希望能够访问列表的不同子集(即 >67%、>90%)等,我会使用 bisect。
  • 请注意,第一个样本之所以有效,是因为原始输入列表已排序,或者更确切地说,因为它满足“所有小于或等于 2 的数字出现在所有大于 2 的数字之前”的较弱条件.
  • 我的下一个建议是你现在真的不需要 a.pop :)
【解决方案2】:

请记住,这是 Python,而不是 C,有时它的工作方式不一定直观。这意味着您必须验证您的假设。我已经使用 IPython 的内置 %%timeit 魔法完成了这项工作。

a = [1,2,2,3,4,5]
%timeit b=[x for x in a if x > 2]\
1000000 loops, best of 3: 362 ns per loop
%timeit c=[x for x in a if not (x > 2)]
1000000 loops, best of 3: 371 ns per loop

所以两者的时间都不到 800 ns,但我们在循环上迭代了两次。我们当然不需要这样做吗?以上几种方法怎么样?让我们从分类开始,它非常简单,只遍历列表一次:

%timeit b, a = classify(a, lambda x: x > 2)
1000000 loops, best of 3: 1.89 µs per loop

哇,即使它只遍历循环一次,它所花费的时间也是上述简单解决方案的两倍多,后者遍历了两次。让我们尝试对上面提出的其他解决方案稍作改动:

%%timeit
b, c = [], []
for x in a:
    b.append(x) if x > 2 else a.append(x)
1000000 loops, best of 3: 1.2 µs per loop

更好,但它仍然比我们的“幼稚”/“低效”实现慢。也许稍微不同的配方会更好:

%%timeit
b, c = [], []
for x in a:
    if x > 2:
        b.append(x)
    else:
        c.append(x)
1000000 loops, best of 3: 983 ns per loop

嗯,这看起来几乎一样,但要快一点。仍然没有击败天真的实现。让我们变得非常聪明,也许让它更快:

%%timeit
b, c = [], []
for x in a:
    (b, c)[x > 2].append(x)
1000000 loops, best of 3: 1.28 µs per loop

所以,我们看到的是,尽管我们希望不重复循环两次,但我们似乎无法在仅执行两个列表推导上有所改进。列表推导式在 Python 中有点像“幕后”,因此对于许多事情,它们比你想出的任何事情都要快。

现在,x < 2 比较便宜 - 没有函数调用,简单的数学运算。会有一点开始变得有意义。让我们创建一个更昂贵的比较函数 - 我们将计算阶乘(并且效率低下):

def factorial(x):
    if x in (0,1):
        return 1
    else:
        return x * factorial(x-1)

%timeit b = [x for x in a if factorial(x) > 6]
100000 loops, best of 3: 3.47 µs per loop
%timeit c = [x for x in a if not factorial(x) > 6]
100000 loops, best of 3: 3.53 µs per loop

所以,显然时间变长了一点 - 现在一切都在 7uS 左右。

现在让我们尝试一些其他示例:

%timeit b, c = classify(a, lambda x: factorial(x) > 6)
100000 loops, best of 3: 5.05 µs per loop

%%timeit
b, c = [], []
for x in a:
    if factorial(x) > 6:
        b.append(x)
    else:
        c.append(x)
100000 loops, best of 3: 4.01 µs per loop

%%timeit
b, c = [], []
for x in a:
    (b, c)[factorial(x) > 6].append(x)
100000 loops, best of 3: 4.59 µs per loop

所有这些的教训:在处理 python 和效率时,在控制台中尝试通常是一个好主意,但通常天真的实现具有合理的性能并且最容易阅读。快速测试会告诉您尝试优化是否真的值得;如果您不小心,通常会使其可读性降低且速度变慢....

附录:有人评论说我们需要更长的列表,因为我们衡量的是函数调用开销而不是性能。他们有一个很好的观点,但时间在我的机器上显示出大致相同的关系:

In [16]: a = range(100000)

In [17]: random.shuffle(a)

In [18]: %timeit b = [x for x in a if x > 50000]
100 loops, best of 3: 5.2 ms per loop

In [19]: %timeit c = [x for x in m if not x > 50000]
100 loops, best of 3: 5.18 ms per loop

In [20]: %timeit c = [x for x in m if x <= 50000]
100 loops, best of 3: 5.35 ms per loop

In [21]: %%timeit
   ....: b, c = [], []
   ....: for x in a:
   ....:     if x > 50000:
   ....:         b.append(x)
   ....:     else:
   ....:         c.append(x)
   ....:
100 loops, best of 3: 12.7 ms per loop

请注意,如果我将比较更改为 x > 2(而不是 x > 50000),第二个循环会加速到大约 11.4 毫秒。但是,这仍然比简单的实现快一点。也就是说,它可能是我更喜欢的那个——它仍然很容易阅读,而且速度并不慢(或明显慢)。而且随着时间的推移,代码往往会变得更加复杂,因此当您稍后添加另一个比较时,或者将其更改为函数调用时,这样做会更容易,而且在这个版本中性能受到的影响也比单纯的版本要小。

但同样:这并不是说“使用列表推导式”(尽管它们通常是个好主意),而是验证您的假设。

【讨论】:

  • 将数组大小设置为更大会很快显示迭代两次和一次之间的差异。您所掌握的只是各种函数调用的开销,而不是迭代速度。
  • @user3467349:你确定吗?当我在随机生成的 1000 个元素列表上尝试此操作时,我得到了类似的结果
  • @zehnpaard 常规 (b).append 对我来说快了 25%+,列表中只有 100 个值。这并不是说科利在他所说的要点上是不正确的 - 但从这里提出的问题(在 SF 上)我认为用户不满意天真地在同一个数组上运行两次迭代以拆分列表。您可能会说他们应该使用 c-call 而不是 python - 但是在很多情况下(即 NLP),您已经拥有类似 python 的对象并且使用 c-call 不会那么简单。当然,条件越复杂,双重迭代越慢。
  • 我从较长的列表中添加了一些数据——从我在这里看到的情况来看,普遍的共识仍然是相同的——一个遍历一次的 for 循环,与一对列表推导的性能大致相同。但是,您是对的 - 简短的示例可能是有偏差的,而较长的示例会减少开销,并且更多地满足您的需求。我的观点并不是真正的“使用列表推导”,而是实际衡量它——有几次我有一个想法来加速一些产生相反效果的东西;我只知道,因为我测量过。
  • @CorleyBrigman 实际上,我知道发生了什么——我在 python3 中运行数字,而你在 python2 中。
【解决方案3】:

如果您想“弹出”列表 a 中的值并将这些值附加到 b 我会遵循这种方法:

    a = [1,2,2,3,4,5]
    b = []

    for i in a[:]:
        if i > 2:
            b.append(i)
            a.remove(i)

    #a = [1,2,2]
    #b = [3,4,5]

如果列表a 没有按特定顺序排列,此脚本特别有用。

【讨论】:

  • a.remove(i)是一个O(n)运算,所以整个代码是O(n^2)
  • @zehnpaard 你能解释一下这意味着什么,因为我对编程世界很陌生。
  • 算法复杂性是一个很难在评论中涵盖的话题,但粗略的要点是,如果你将 a 中的元素加倍,其他解决方案需要两倍的时间,但你的代码需要四倍在最坏的情况下很长。将输入增加三倍,您的代码将花费 9 倍的时间。原因是虽然不是很明显,但实际上嵌套了两个循环 - 一个隐藏在 a.remove(i) 中,每次删除一个项目时,该项目之后的每个元素都需要在内存中移动。 a.remove(i) 是一项代价高昂的操作,通常应尽可能避免。
  • 你可能想从this SO post开始
【解决方案4】:

只需使用列表推导过滤掉不需要的项目:

a = [1, 2, 2, 3, 4, 5]
condition = lambda x: x > 2
b = [x for x in a if condition(x)]      # b == [3, 4, 5] 
a = [x for x in a if not condition(x)]  # a == [1, 2, 2]

更新

如果您担心效率,那么这是另一种只扫描列表一次的方法:

def classify(iterable, condition):
    """ Returns a list that matches the condition and a list that
    does not """
    true_list = []
    false_list = []
    for item in iterable:
        if condition(item):
            true_list.append(item)
        else:
            false_list.append(item)
    return true_list, false_list

a = [1, 2, 2, 3, 4, 5]
b, a = classify(a, lambda x: x > 2)
print(a)  # [1, 2, 2]
print(b)  # [3, 4, 5]

更新 2

我没有意识到我的更新看起来和 user3467349 几乎一样,但信不信由你,我没有作弊:-)

【讨论】:

  • 这似乎是最易读的,并且与我的问题的 cmets 中相关链接中的最佳答案一致(除了它没有使用 lambda,这是一个非常好的触摸 IMO )。唯一的问题是我不想重复列表两次,这种方式违反了 DRY 原则。虽然它可能是最易读的版本,但我的这部分代码是运行时关键的(但不至于需要另一种语言)。
  • 你无缘无故地对同一个列表进行了两次迭代。
  • @user3467349:这不是no的原因;这是为了可读性。有时这比执行速度更重要。但有时执行速度更重要。或者有时是记忆。这取决于。但可读性绝对值得。
  • 我会说我喜欢这个答案的地方是我可以很容易地将它放入一个以 lambda 函数作为参数的函数中。然后该函数可用于不同的对象类型和相应的条件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多