Python如何如此快速地从列表中删除元素?

Python如何如此快速地从列表中删除元素?,python,optimization,time-complexity,python-internals,Python,Optimization,Time Complexity,Python Internals,我现在正在学习Python,并试图了解容器在实践中是如何工作的。 有一个问题我无法解释。 假设我创建了一个非常大的列表: >>> l = [i for i in range(100000000)] # ~3 sec 创建它大约需要3秒钟,我使用升序数字而不是相同的值来避免可能的优化 如我们所知,删除上的运营成本。但是,当我从列表中间删除一个元素时,它立即返回的速度与任何其他简单命令(如element access)一样快 >>> del l[50000000

我现在正在学习Python,并试图了解容器在实践中是如何工作的。 有一个问题我无法解释。 假设我创建了一个非常大的列表:

>>> l = [i for i in range(100000000)] # ~3 sec
创建它大约需要3秒钟,我使用升序数字而不是相同的值来避免可能的优化

如我们所知,删除上的运营成本。但是,当我从列表中间删除一个元素时,它立即返回的速度与任何其他简单命令(如element access)一样快

>>> del l[50000000] # instantly (< 0.1 sec)
在这之后,我可以在删除后不到3秒内访问元素l[25000000]和l[75000000],而且它也会立即执行,所以我不能用延迟或背景删除来解释这一点

请有人给我解释一下,内部是怎么做的?列表实际上是作为某种树实现的吗?这听起来很奇怪,而且,它会违反对的要求

< P>这是一个常见的优化,比如C++中的返回值优化,还是一些稀有的,只针对我的平台/版本?
我使用Linux和Python 3.4.1 Python 2.7.9显示了相同的结果。

我不知道为什么您认为删除单个元素需要3秒钟

您的初始时间是100000000个单独的追加操作。每一个都需要几分之一秒的时间;删除操作所需的时间与此类似


在任何情况下,正如Bartosz指出的那样,复杂度并不意味着所有操作都需要相同的时间长度,而是意味着时间长度与列表的长度成正比。

我不知道为什么您认为删除单个元素需要3秒钟

您的初始时间是100000000个单独的追加操作。每一个都需要几分之一秒的时间;删除操作所需的时间与此类似


在任何情况下,正如巴托斯指出的那样,复杂度并不意味着所有操作都需要相同的时间长度,它意味着时间长度与列表的长度成正比。

我决定将我的注释集转换为正确的答案

首先,让我们澄清一下当您这样做时发生了什么:

>>> l = [i for i in range(100000000)]
这里发生了三件事:

正在创建100000000个int对象。在CPython中创建对象需要分配内存并将内容放入该内存,这需要时间。 您正在运行一个循环。这对性能有很大影响:[i for i in range…]比listrange慢得多。。。。 正在动态创建大型列表。 阅读你的问题,你似乎只考虑了最后一点,忽略了其他的。因此,您的计时是不准确的:创建一个大列表不需要3秒钟,它只需要这3秒钟的一小部分

这个分数有多大是一个有趣的问题,仅使用Python代码很难回答,但我们仍然可以尝试。具体而言,我将尝试以下陈述:

>>> [None] * 100000000
在这里,CPython不必创建大量的对象,只有一个对象,不必运行循环,并且可以为列表分配一次内存,因为它预先知道大小

时间安排是不言自明的:

$ python3 -m timeit "list(range(100000000))"
10 loops, best of 3: 2.26 sec per loop
$ python3 -m timeit "[None] * 100000000"
10 loops, best of 3: 375 msec per loop
现在,回到你的问题:删除项目怎么样

$ python3 -m timeit --setup "l = [None] * 100000000" "del l[0]"
10 loops, best of 3: 89 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 4]"
10 loops, best of 3: 66.5 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 2]"
10 loops, best of 3: 45.3 msec per loop
这些数字告诉我们一些重要的事情。注意,2×45.3≈ 89也是66.5×4/3≈ 89

这些数字准确地说明了线性复杂性的含义。如果函数的时间复杂度kn为On,则表示如果我们将输入加倍,则时间加倍;如果我们将输入大小增加4/3,则时间将增加4/3

这就是这里发生的事情。在CPython中,100000000项的列表是一个连续的内存区域,包含指向Python对象的指针:

l = |ptr0|ptr1|ptr2|...|ptr99999999|
当我们运行dell[0]时,我们将ptr1从右向左移动,覆盖ptr0。其他元素也一样:

l = |ptr0|ptr1|ptr2|...|ptr99999999|
     ^^^^
         ` item to delete

l = |ptr1|ptr2|...|ptr99999999|

因此,当我们运行dell[0]时,我们必须向左移动99999998个指针。这与dell[100000000//2]不同,dell[100000000//2]只需要移动一半的指针,而前一半的指针不需要移动。移动一半的指针等于执行一半的操作,这大致意味着在一半的时间内运行这并不总是正确的,但计时表明在这种情况下这是正确的。

我决定将我的注释集转化为正确的答案

首先,让我们澄清一下当您这样做时发生了什么:

>>> l = [i for i in range(100000000)]
这里发生了三件事:

正在创建100000000个int对象。在CPython中创建对象需要分配内存并将内容放入该内存,这需要时间。 您正在运行一个循环。这对性能有很大影响:[i for i in range…]比listrange慢得多。。。。 正在动态创建大型列表。 阅读你的问题,你似乎只考虑了最后一点,忽略了其他的。因此,您的计时是不准确的:创建一个大列表不需要3秒钟,它只需要这3秒钟的一小部分

有多大 这个分数是一个有趣的问题,仅使用Python代码很难回答,但我们仍然可以尝试。具体而言,我将尝试以下陈述:

>>> [None] * 100000000
在这里,CPython不必创建大量的对象,只有一个对象,不必运行循环,并且可以为列表分配一次内存,因为它预先知道大小

时间安排是不言自明的:

$ python3 -m timeit "list(range(100000000))"
10 loops, best of 3: 2.26 sec per loop
$ python3 -m timeit "[None] * 100000000"
10 loops, best of 3: 375 msec per loop
现在,回到你的问题:删除项目怎么样

$ python3 -m timeit --setup "l = [None] * 100000000" "del l[0]"
10 loops, best of 3: 89 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 4]"
10 loops, best of 3: 66.5 msec per loop
$ python3 -m timeit --setup "l = [None] * 100000000" "del l[100000000 // 2]"
10 loops, best of 3: 45.3 msec per loop
这些数字告诉我们一些重要的事情。注意,2×45.3≈ 89也是66.5×4/3≈ 89

这些数字准确地说明了线性复杂性的含义。如果函数的时间复杂度kn为On,则表示如果我们将输入加倍,则时间加倍;如果我们将输入大小增加4/3,则时间将增加4/3

这就是这里发生的事情。在CPython中,100000000项的列表是一个连续的内存区域,包含指向Python对象的指针:

l = |ptr0|ptr1|ptr2|...|ptr99999999|
当我们运行dell[0]时,我们将ptr1从右向左移动,覆盖ptr0。其他元素也一样:

l = |ptr0|ptr1|ptr2|...|ptr99999999|
     ^^^^
         ` item to delete

l = |ptr1|ptr2|...|ptr99999999|

因此,当我们运行dell[0]时,我们必须向左移动99999998个指针。这与dell[100000000//2]不同,dell[100000000//2]只需要移动一半的指针,而前一半的指针不需要移动。移动一半的指针等于执行一半的操作,这大致意味着在一半的时间内运行这并不总是正确的,但计时表明在这种情况下是正确的。

1时间复杂度不是这个意思。2展示你正在进行基准测试的完整代码3展示你是如何做到这一点的。这个列表实际上是以某种树的形式实现的吗没有,而且也没有进行背景删除。Python是开源的,您可以看到它的实现:我使用的是Python解释器,而不是单独的程序。我已经提供了我键入的所有代码。在时间复杂性下,我指的是操作的渐进复杂性,而不是它们执行所需的时间。@Vasily是的,完全是。您试图了解容器是如何工作的,这是一种方法。还要注意的是,您需要在注释中包含@username,以通知其他用户您正在对其进行寻址。listobject.c源代码非常容易阅读,前提是您对c语法有一定的了解。1这不是时间复杂性的含义。2展示你正在进行基准测试的完整代码3展示你是如何做到这一点的。这个列表实际上是以某种树的形式实现的吗没有,而且也没有进行背景删除。Python是开源的,您可以看到它的实现:我使用的是Python解释器,而不是单独的程序。我已经提供了我键入的所有代码。在时间复杂性下,我指的是操作的渐进复杂性,而不是它们执行所需的时间。@Vasily是的,完全是。您试图了解容器是如何工作的,这是一种方法。还请注意,您需要在注释中包含@username,以通知其他用户您正在对其进行寻址。listobject.c源代码非常容易阅读,前提是您对c语法有一定的了解。值得注意的是,基础对象将在向上的过程中调整大小,但在返回到半满之前不会调整大小,这增加了更多的开销。所以,你的答案是没有复杂性问题,只是相同的复杂性和不同的操作时间?是的,我同意,我的初始时间是10m*timeappend。但当我从数组中间移除元素时,我必须移动5米的元素,否则我就失去了在固定时间访问元素的可能性。所以,时间应该是5m*timeassign。这两种操作都是缓存友好的。我无法想象为什么用计数器值分配元素的时间要比用相邻值分配元素的时间高50倍。@瓦西里,但这并不是全部情况。删除只不过是移动连续内存块中的现有引用,而创建列表还需要创建列表要引用的对象,并调整列表大小以适应所有对象。@jornsharpe是的,这是一个有趣的想法。我认为创建引用对象所需的时间可以忽略不计。但现在我尝试先用零初始化整个列表,然后用1分配它们。这是相当快的0.3秒。尽管如此,仍然明显慢于删除。看起来答案可能比我想象的更复杂。不仅仅是复杂的删除逻辑,还有一些Python内存管理。值得注意的是,底层对象会在上升的过程中调整大小,但不会在下降的过程中调整大小,直到达到半满为止,这会增加更多的开销。因此,您的答案是不存在复杂性问题,只是不同操作时间的复杂性相同?是的,我同意,我的初始时间是10m*timeappend。但当我从数组中间移除元素时,我必须移动5米的元素,否则我就失去了在固定时间访问元素的可能性。所以,时间应该是5m*timeassign。这两种操作都是缓存友好的。我无法想象
为什么用计数器值分配元素的时间应该比用相邻值分配元素的时间高50倍。@Vasily,但这并不是全部情况。删除只不过是移动连续内存块中的现有引用,而创建列表还需要创建列表要引用的对象,并调整列表大小以适应所有对象。@jornsharpe是的,这是一个有趣的想法。我认为创建引用对象所需的时间可以忽略不计。但现在我尝试先用零初始化整个列表,然后用1分配它们。这是相当快的0.3秒。尽管如此,仍然明显慢于删除。看起来答案可能比我想象的更复杂。不仅是复杂的删除逻辑,还有一些Python内存管理。因此,如果对象的分配比分配引用慢6倍,那么使用无引用的对象进行分配和初始化比移动它们慢8倍。嗯,我想我能继续下去。尽管这对我来说似乎是一个相当大的开销。除此之外,阅读@jornsharpe建议的代码可以发现列表是创建的,而主要使用memmove。因此,如果对象的分配比分配引用慢6倍,则使用无引用的分配和初始化比移动它们慢8倍。嗯,我想我能继续下去。尽管这对我来说似乎是一个很大的开销。除此之外,阅读@jornsharpe建议的代码可以发现列表是创建的,而主要使用memmove。