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

Chr*_*ris 65 python list slice

我有以下问题.

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

我如何在Python中实现这一目标?以某种方式使用缓冲对象?

sen*_*rle 98

简短的回答

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

答案很长

测试可变和不可变的值

首先,让我们测试基本声明.我们可以证明,即使在整数等不可变对象的情况下,也只复制引用.这是三个不同的整数对象,每个对象具有相同的值:

>>> a = [1000 + 1, 1000 + 1, 1000 + 1]
Run Code Online (Sandbox Code Playgroud)

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

>>> map(id, a)
[140502922988976, 140502922988952, 140502922988928]
Run Code Online (Sandbox Code Playgroud)

切片时,引用保持不变.没有创建新对象:

>>> b = a[1:3]
>>> map(id, b)
[140502922988952, 140502922988928]
Run Code Online (Sandbox Code Playgroud)

使用具有相同值的不同对象表明复制过程不会打扰实习 - 它只是直接复制引用.

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

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

检查剩余内存开销

当然,复制引用本身.每个在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
Run Code Online (Sandbox Code Playgroud)

正如Joe Pinsonault 提醒我们的那样,这种开销会增加.整数对象本身不是很大 - 它们比引用大三倍.因此,这绝对意义上可以节省一些内存,但渐渐地,能够将多个列表"视图"放入同一个内存中可能会很好.

使用视图保存内存

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

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

>>> a = numpy.arange(3)
>>> a
array([0, 1, 2])
>>> b = a[1:3]
>>> b
array([1, 2])
Run Code Online (Sandbox Code Playgroud)

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

>>> a[2] = 1001
>>> b
array([   1, 1001])
Run Code Online (Sandbox Code Playgroud)

但这意味着您必须确保在修改一个对象时,您不会无意中修改另一个对象.这是你使用时的权衡numpy:减少计算机的工作量,为程序员做更多的工作!

  • 虽然答案是正确的,但这个例子实际上并没有证明它,因为小整数被实习; 尝试做`id(2)`甚至`id(1 + 1)`.一个更好的例子是使用`a = [[],[],[]]`. (4认同)
  • 在不可变对象(例如元组)中,引用是不可变的,但它们引用的项可以是可变的.因此,无法更改3个列表的元组,它将始终引用相同的3个列表,但每个列表的内容可以像任何列表一样更改. (3认同)
  • 或者实际上,在进一步阅读后,问题实际上“指定”列表是由整数组成的,所以我觉得很好奇,作者甚至一开始就担心项目副本!(我宁愿认为OP没有完全理解您的澄清请求,实际上想获得原始列表的“意见”) (2认同)
  • 这个答案是正确的,但我认为值得指出的是,如果您有非常大的数组,复制指针数组仍然会很昂贵 (2认同)

Amb*_*ber 20

根据您正在做的事情,您可以使用islice.

由于它通过迭代操作,因此它不会创建新列表,而是简单地创建迭代器,即yield原始列表中的元素按其范围请求.

  • 这里的坏处是islice没有利用实现__getitem__方法的对象,将所有东西视为迭代器,因此它将始终从列表的第一个元素迭代,直到它到达列表的第一个位置开始屈服范围内的值. (5认同)

gat*_*ich 8

一般来说,列表切片是最好的选择。

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

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')
Run Code Online (Sandbox Code Playgroud)

结果:

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
Run Code Online (Sandbox Code Playgroud)


Mat*_*haq 6

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

def listslice(xs, *args):
    for i in range(len(xs))[slice(*args)]:
        yield xs[i]
Run Code Online (Sandbox Code Playgroud)

用法:

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

>>> for x in listslice(xs, 2, 4):
...     print(x)
4
6
Run Code Online (Sandbox Code Playgroud)