在Python中切片列表而不生成副本


76

我有以下问题。

给定一个整数列表L,我需要生成所有子列表L[k:] for k in [0, len(L) - 1]而不生成副本

如何在Python中完成此操作?用某种缓冲对象?


您是否不想复制列表本身,还是不想复制其中的对象?
senderle'2

在生成切片时,我不想在其中复制对象。
克里斯,

默认情况下,Python不制作副本。
琥珀

1
您怎么知道正在复印?您注意到资源增加了吗?
mt3

Answers:


117

简短的答案

切片列表不会生成列表中对象的副本;它只是将引用复制到它们。这就是所问问题的答案。

长答案

测试可变和不变的值

首先,让我们测试一下基本主张。我们可以证明,即使在整数之类的不可变对象的情况下,也仅复制了引用。这是三个不同的整数对象,每个都有相同的值:

>>> a = [1000 + 1, 1000 + 1, 1000 + 1]

它们具有相同的值,但是您可以看到它们是三个不同的对象,因为它们具有不同的ids:

>>> map(id, a)
[140502922988976, 140502922988952, 140502922988928]

对它们进行切片时,引用保持不变。尚未创建新对象:

>>> b = a[1:3]
>>> map(id, b)
[140502922988952, 140502922988928]

使用具有相同值的不同对象表明复制过程不会为interner困扰,它只是直接复制引用。

使用可变值进行测试会得到相同的结果:

>>> a = [{0: 'zero', 1: 'one'}, ['foo', 'bar']]
>>> map(id, a)
[4380777000, 4380712040]
>>> map(id, a[1:]
... )
[4380712040]

检查剩余的内存开销

当然,引用本身也会被复制。在64位计算机上,每个字节花费8个字节。每个列表都有其自己的72字节的内存开销:

>>> for i in range(len(a)):
...     x = a[:i]
...     print('len: {}'.format(len(x)))
...     print('size: {}'.format(sys.getsizeof(x)))
... 
len: 0
size: 72
len: 1
size: 80
len: 2
size: 88

正如Joe Pinsonault提醒我们的那样,开销加起来了。整数对象本身不是很大-它们比引用大三倍。因此,这从绝对的意义上为您节省了一些内存,但是,渐近地,能够将多个“视图”列表放入同一内存可能会很好。

使用视图节省内存

不幸的是,Python没有提供简单的方法来产生作为列表“视图”的对象。也许我应该说“幸运”!这意味着您不必担心切片的来源;更改为原始图像不会影响切片。总的来说,这使得对程序行为的推理变得容易得多。

如果您确实想通过使用视图来节省内存,请考虑使用numpy数组。切片numpy数组时,切片和原始切片之间共享内存:

>>> a = numpy.arange(3)
>>> a
array([0, 1, 2])
>>> b = a[1:3]
>>> b
array([1, 2])

当我们修改a并再次查看时会发生什么b

>>> a[2] = 1001
>>> b
array([   1, 1001])

但这意味着您必须确保在修改一个对象时,不要无意间修改了另一个对象。这是您使用时的折衷方案numpy:计算机工作量减少,程序员工作量增加!


3
在不可变对象(例如元组)中,引用是不可变的,但它们引用的项可以是可变的。因此,不能更改包含3个列表的元组,它将始终引用相同的3个列表,但是每个列表的内容可以像在任何列表中一样进行更改。
trichoplax

6
尽管答案是正确的,但是该示例并未实际演示它,因为会插入小整数。尝试做id(2)甚至做id(1+1)。一个更好的例子是使用a = [[], [], []]
Exp HP

1
或者,实际上,在进一步阅读之后,该问题实际上指定了该列表是由整数组成的,因此我非常好奇作者甚至担心项目的开头!(我会更快地认为OP不能完全理解您的澄清要求,而实际上想在原始列表中获得“意见”)
Exp HP

1
这个答案是正确的,但是我认为值得指出的是,如果您有非常大的数组,则复制指针数组仍然会很昂贵
Joe Pinsonault

@ExpHP,那可能是真的。我一直在努力不要太long,但是对于这样的问题,我想那是不可能的!编辑。
senderle '16

27

根据您的操作,您可能可以使用islice

由于它是通过迭代操作的,因此它不会创建新列表,而只会yield根据其范围的要求从原始列表中创建元素的迭代器。


8
不好的是,islice没有利用实现getitem方法的对象,而是将所有内容都视为迭代器,因此它将始终从列表的第一个元素进行迭代,直到到达列表的第一个位置以开始产生范围内的值。
jgomo3

4

一个简单的替代方法islice是不遍历不需要的列表项:

def listslice(xs, *args):
    for i in range(len(xs))[slice(*args)]:
        yield xs[i]

用法:

>>> xs = [0, 2, 4, 6, 8, 10]

>>> for x in listslice(xs, 2, 4):
...     print(x)
4
6


0

通常,列表切片是最佳选择。

这是一个快速的性能比较:

from timeit import timeit
from itertools import islice

for size in (10**4, 10**5, 10**6):
    L = list(range(size))
    S = size // 2
    def sum_slice(): return sum(L[S:])
    def sum_islice(): return sum(islice(L, S, None))
    def sum_for(): return sum(L[i] for i in range(S, len(L)))

    assert sum_slice() == sum_islice()
    assert sum_slice() == sum_for()

    for method in (sum_slice, sum_islice, sum_for):
        print(f'Size={size}, method={method.__name__}, time={timeit(method, number=1000)} ms')

结果:

Size=10000,   method=sum_slice,  time=0.0298 ms
Size=10000,   method=sum_islice, time=0.0449 ms
Size=10000,   method=sum_for,    time=0.2500 ms
Size=100000,  method=sum_slice,  time=0.3262 ms
Size=100000,  method=sum_islice, time=0.4492 ms
Size=100000,  method=sum_for,    time=2.4849 ms
Size=1000000, method=sum_slice,  time=5.4092 ms
Size=1000000, method=sum_islice, time=5.1139 ms
Size=1000000, method=sum_for,    time=26.198 ms
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.