在Python中切片列表而不生成副本
我遇到了一个问题。
给定一个整数列表
L
,我需要生成所有的子列表L[k:]
,其中k
的范围是从 0 到len(L) - 1
,而且不能生成副本。
我该如何在 Python 中做到这一点?是不是可以用某种缓冲对象?
5 个回答
这是一个简单的替代方案,用来代替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
根据你要做的事情,你可能可以使用 islice
。
因为它是通过迭代来工作的,所以不会生成新的列表,而是会创建一些迭代器,这些迭代器会根据你请求的范围,从原始列表中“产出”元素。
简短回答
切片列表并不会生成列表中对象的副本;它只是复制了这些对象的引用。这就是问题的答案。
详细回答
测试可变和不可变值
首先,我们来验证这个基本说法。即使是像整数这样的不可变对象,复制的也只是引用。这里有三个不同的整数对象,它们的值都是一样的:
>>> a = [1000 + 1, 1000 + 1, 1000 + 1]
它们的值相同,但你可以看到它们是三个不同的对象,因为它们的 id
不同:
>>> map(id, a)
[140502922988976, 140502922988952, 140502922988928]
当你对它们进行切片时,引用保持不变。没有创建新的对象:
>>> b = a[1:3]
>>> map(id, b)
[140502922988952, 140502922988928]
使用不同的对象但值相同,说明复制过程并不关心 字符串的内部化 — 它只是直接复制引用。
用可变值进行测试也得出相同的结果:
>>> 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
的权衡:计算机的工作量减少了,但程序员的工作量增加了!