我有以下问题。
给定一个整数列表
,我需要生成所有子列表L
L[k:]
,而不生成副本。for k in [0, len(L) - 1]
如何在 Python 中完成此操作?以某种方式使用缓冲区对象?
切片列表不会生成列表中对象的副本;它只是复制对它们的引用。这就是所问问题的答案。
首先,让我们测试一下基本声明。我们可以证明,即使在像整数这样的不可变对象的情况下,也只会复制引用。这是三个不同的整数对象,每个对象都具有相同的值:
>>> a = [1000 + 1, 1000 + 1, 1000 + 1]
它们具有相同的值,但您可以看到它们是三个不同的对象,因为它们具有不同的
id
s:
>>> map(id, a)
[140502922988976, 140502922988952, 140502922988928]
当你切片它们时,引用保持不变。没有创建新对象:
>>> b = a[1:3]
>>> map(id, b)
[140502922988952, 140502922988928]
使用具有相同值的不同对象表明复制过程不会打扰interning——它只是直接复制引用。
使用可变值进行测试会得到相同的结果:
>>> 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
时的权衡:计算机的工作量减少,程序员的工作量增加!
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
一般来说,列表切片是最好的选择。
这是一个快速的性能比较:
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
我写了一个
ListView
类,它甚至避免复制列表的脊椎:
https://gist.github.com/3noch/b5f3175cfe39aea71ca4d07469570047
这支持嵌套切片,以便您可以继续切片到视图中以获得更窄的视图。例如:
ListView(list(range(10)))[4:][2:][1] == 7
.
请注意,这还没有完全成熟,当底层列表与测试套件一起发生变化时,需要进行更多的错误检查。
gatopeich 在基准测试方面做得很好,可以改进的一件事是使用
map(func, iterable)
而不是 (func(x) for x in iterable)
:
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)))
def sum_for_map(): return sum(map(L.__getitem__, range(S, len(L))))
assert sum_slice() == sum_islice()
assert sum_slice() == sum_for()
assert sum_slice() == sum_for_map()
for method in (sum_slice, sum_islice, sum_for, sum_for_map):
print(f'Size={size}, method={method.__name__}, time={timeit(method, number=1000)} ms')
Size=10000, method=sum_slice, time=0.03461450000759214 ms
Size=10000, method=sum_islice, time=0.04916659998707473 ms
Size=10000, method=sum_for, time=0.23745350004173815 ms
Size=10000, method=sum_for_map, time=0.15346350008621812 ms
Size=100000, method=sum_slice, time=0.7442841000156477 ms
Size=100000, method=sum_islice, time=0.747725100023672 ms
Size=100000, method=sum_for, time=2.6151005999417976 ms
Size=100000, method=sum_for_map, time=1.7780358999734744 ms
Size=1000000, method=sum_slice, time=13.451647999929264 ms
Size=1000000, method=sum_islice, time=11.885209699976258 ms
Size=1000000, method=sum_for, time=30.072602799977176 ms
Size=1000000, method=sum_for_map, time=20.923258899943903 ms