在python中删除多个列表中的几个项目的最有效方法?

时间:2018-04-06 15:35:14

标签: python list

我有几个项目列表。没有重复项,每个项目最多出现一次(通常,在所有列表中只出现一次)。我还有一个要从此数据集中删除的项目列表。如何以最干净,最有效的方式完成?

我已经读过,在python中,创建一个新对象通常比过滤一个新对象更简单,更快。但我在基本测试中没有注意到这一点:

data = [[i*j for j in range(1, 1000)] for i in range(1, 1000)]
kill = [1456, 1368, 2200, 36, 850, 9585, 59588, 60325, 9520, 9592, 210, 3]

# Method 1 : 0.1990 seconds
for j in kill:
    for i in data:
        if j in i:
            i.remove(j)

# Method 2 : 0.1920 seconds
for i in data:
    for j in kill:
        if j in i:
            i.remove(j)

# Method 3 : 0.2790 seconds
data = [[j for j in i if j not in kill] for i in data]

哪种方法最适合在Python中使用?

2 个答案:

答案 0 :(得分:5)

https://wiki.python.org/moin/TimeComplexity

remove是O(n),因为它首先在列表中线性搜索,然后,如果找到它,则删除的对象之后的每个元素都会在内存中向左移动一个位置。因为remove这是一项非常昂贵的操作。

因此,从M N O(N*M)长度列表中删除in个项目 列表上的

O(n)也是O(N*M),因为我们需要按顺序搜索整个列表。因此,使用过滤器构建新列表也是in。但是,由于哈希使我们的过滤器O(1)

O(N)套上的def remove_kill_from_data(data, kill): s = set(kill) return [i for i in data if i not in kill] O(N)

因此,最好的解决方案是(为了简单起见,我将使用平面列表,而不是嵌套)

def remove_kill_from_data_unordered(data, kill):
    s = set(kill)
    d = set(data)
    return d - s

如果你不关心保持秩序,这会更快(由于在C级完成,它仍然是kill_set = set(kill) [remove_kill_from_data(d, kill_set) for d in data]

data

申请列表

%timeit method1(data, kill)
210 ms ± 769 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit method2(data, kill)
208 ms ± 2.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit method3(data, kill)
272 ms ± 1.28 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit method4(data, kill)  # using remove_kill_from_data
69.6 ms ± 1.33 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit method5(data, kill) # using remove_kill_from_data_unordered
59.5 ms ± 3.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

某些时间安排(每个副本首先来自静态Private pChildObjects As New Collection Private pName As String Public Property Get ChildObjects() As Collection Set ChildObjects = pChildObjects End Property Public Property Let ChildObjects(value As Collection) Set pChildObjects = value End Property Public Property Get Name() As String Name = pName End Property Public Property Let Name(value As String) pName = value End Property

Sub GenerateHierarchy()
Dim a As clsObj, b As clsObj, c As clsObj

Set a = New clsObj

a.Name = "Grandfather Bob"

Set b = New clsObj
    b.Name = "Parent A"
    a.ChildObjects.Add b, b.Name

Set c = New clsObj
    c.Name = "Child A"
    a.ChildObjects("Parent A").ChildObjects.Add c, c.Name

End Sub

答案 1 :(得分:1)

没有“从Python中删除列表的最佳方法”。如果有的话,Python只有一种方法可以做到。对于不同的问题有不同的最佳方法,这就是为什么Python有不同的方法来做到这一点。

正确性远比速度重要。快速得到错误的答案是没用的。 (否则,最快的解决方案就是什么都不做。)前两个实现有两个问题。

首先,使用remove按值查找和删除元素。除了浪费(你只是搜索整个列表以找到元素,现在你再次搜索它以找到并删除它),如果有任何重复,这是不正确的 - 只有第一个会得到除去。如果不是任何重复项,你可能应该使用一个集合(或者一个OrderedSet,如果没有重复但是顺序很重要),这会让你写这个更简单和非常更快。

其次,您在迭代时从列表中删除。这会导致您错过元素。如果删除元素2,则会将所有后续元素向上移动 - 因此原始元素3现在是元素2,但是下一次循环检查元素3.因此,如果连续有两个可用元素,则第二个一个人会被遗漏。你可以通过反向迭代来解决这个问题,但它会让事情变得更复杂。或者您可以在修改原始文件时迭代副本,但这会使事情变得更复杂,并且需要花费时间和空间来复制副本。

这两个问题都可以修复,但这提出了一个重要的观点:前两个版本更容易出现微妙的错误,事实证明你弄错了它们甚至没有注意到它。

当然,修复这些问题可能会使前两个版本慢一点,而不是更快一点。

即使你解决了这些问题,改变一个对象也不会像创建一个新对象那样做。如果其他人对同一个列表有引用,他们会看到前两个版本的更改,但是他们会保留他们在上一个版本中所期望的列表。如果其他人是另一个可能在您正在处理它的同时迭代列表的线程上的代码,那么事情变得更加复杂。有时您想要第一个行为,有时候是第二个行为。您可以在任一版本上添加更多复杂性以获得相反的效果(例如,将整理分配给整个列表的一部分,而不仅仅是重新绑定名称),但通常直接编写您想要的那个更简单。

另外,理解版本可以简单地改为只能按需工作的迭代版本(只需将一组或两组括号更改为括号)。它适用于任何可迭代的,而不仅仅是列表。通过将算法重写为迭代器转换链,您通常可以在更高级别获得巨大的性能优势和/或简化,因此您永远不需要内存中的整个数据集。但有时候,您可以通过多次传递或随机访问模式获得巨大的性能或简单优势,因此列表要好得多。这将决定您对这段代码的实现。

还有空格差异。理解采用线性临时空间而不是常量,但另一方面,由于Python增长和缩小列表的方式,它可以在内存中留下较小的最终结果。 (如果这很重要,您需要对其进行测试 - 该语言甚至不能保证列表会缩小其存储空间;他们如何这样做取决于每个实现。)

最后,我们谈论的是一个相当小的差异。如果这在您的代码中很重要,那么您忽略了可以提供更大改进的其他选项这一事实可能会更重要。如果您可以使用集合列表而不是列表列表,那么差异将是巨大的。如果你做不到,至少让kill设置速度加快,你绝对可以做到。使用numpy可能会提高一个数量级。只运行PyPy中的现有代码而不是CPython可能会使得它的速度几乎和numpy一样多,而工作量却少得多。或者您可能想为内部循环编写一个C扩展(这可能只是将相同的代码放在.pyx文件中并对其进行Cython化)。如果这些事情似乎都不值得为一个数量级或更好的改进付出努力,为什么值得把你已经投入的时间用于50%的改进呢?

为此添加一些实际数字:

  • 方法1:140毫秒
  • 纠正方法1:193ms
  • 方法3:190毫秒
  • PyPy中的方法3:21.6ms
  • [i - kill for i in data]其中data是一个集合列表,而kill是一个集合:20.6ms
  • data[~np.isin(data, kill)]其中datanp.array:26.6ms

(我也在Python 2.7中尝试了相同的测试;方法3慢了大约30%,方法4大约慢了15%,而其他方法几乎相同。)

作为旁注,您没有向我们展示您是如何测试此代码的,并且测试很容易出错。即使你使用timeit,你仍然需要确保每次都对原始列表运行,而不是针对同一个已经过滤的列表重复代码(这意味着第一个代表正在测试正确的情况,其他99999名代表正在测试一个不存在可杀戮的情况。