GenExp中的Tuple()与ListComp的比较
我有一个小列表,里面放了一些项目,比如:
my_list = [1,2,3,4,5,6,7,8,9,10]
还有一个包含索引的元组,比如:
indexes = (1,5,9)
我想从这个列表中提取出对应的值,变成一个元组,比如:
tuple(my_list[x] for x in indexes)
但是这样做的速度很慢,尤其是当我多次运行的时候。
这个索引的元组在我处理的每个列表中都是一样的,那有没有更快的方法呢?
我现在用的是Python 2.5,得到了一些让我意外的结果:
python -m timeit -s "indexes = (1,5,9); l = [1,2,3,4,5,6,7,8,9,10]" "tuple(l[i] for i in indexes)"
100000 loops, best of 3: 3.02 usec per loop
python -m timeit -s "indexes = (1,5,9); l = [1,2,3,4,5,6,7,8,9,10]" "tuple([l[i] for i in indexes])"
1000000 loops, best of 3: 0.707 usec per loop
这是个例外情况,还是说列表推导式真的比生成器表达式要好很多呢?
3 个回答
另一个选择,虽然比delnan的方法慢,是使用 __getitem__
和 map
一起。不过,即使加了导入语句,delnan的方法还是更快。
In [36]: %timeit tuple(map(my_list.__getitem__,indexes))
1000000 loops, best of 3: 653 ns per loop
In [38]: %timeit itemgetter(*indexes)(my_list)
1000000 loops, best of 3: 292 ns per loop
没有ipython的情况下:
python -m timeit -s "indexes = (1,5,9); l = [1,2,3,4,5,6,7,8,9,10]" "tuple(map(l.__getitem__,indexes))"
1000000 loops, best of 3: 0.645 usec per loop
python -m timeit -s "import operator" "indexes = (1,5,9); l = [1,2,3,4,5,6,7,8,9,10]" "operator.itemgetter(*indexes)(l)"
1000000 loops, best of 3: 0.463 usec per loop
看起来转换成元组让使用map的方法比使用itemgetter的方法慢:
python -m timeit -s "indexes = (1,5,9); l = [1,2,3,4,5,6,7,8,9,10]" "map(l.__getitem__,indexes)"
1000000 loops, best of 3: 0.489 usec per loop
元组是一种不可变的序列,这意味着一旦创建(并分配了内存),它就需要先知道里面会有多少个元素。这就导致了从生成器表达式创建元组时,生成器必须先完全迭代一次。而生成器只能被消费一次,所以这些元素需要存储在某个地方。可以把这个过程想象成这样:
tuple(list(generator))
现在,从生成器表达式创建列表的速度比用列表推导式创建列表要慢,所以你可以通过使用列表推导式来节省时间。
如果你没有特别的理由需要使用元组,也就是说,如果你不需要不可变的特性,你可以选择保留列表,而不是把它转换成元组,这样可以节省更多时间。
最后,不,实际上没有比遍历索引并对每个索引查询序列更好的方法。即使索引总是相同,它们仍然需要对每个列表进行评估,所以你无论如何都得重复这个过程。
不过,如果这些索引是固定的,你可以节省更多时间。因为简单的 (l[1], l[5], l[9])
会比其他任何方法都快得多;)
以下是一些来源的参考(这里使用的是3.4版本,但在2.x版本中应该类似):
使用内置的 tuple()
函数创建元组是在函数 PySequence_Tuple
中完成的。
如果参数是一个列表,Python会通过调用 PyList_AsTuple
来明确处理,这个过程基本上是分配一个与列表长度相同的元组,然后将所有项目复制过去。
否则,它会从参数创建一个迭代器,并首先尝试 猜测 长度。由于生成器没有长度,Python会使用默认的猜测值 10
来分配一个这个长度的元组——注意,对于你的元组,我们分配了多余的7个空间。接着,它会迭代这个迭代器,并将每个值分配到元组中的相应位置。之后,它会 调整 创建的元组的大小。
现在,实际的区别可能在于列表推导式的工作方式。列表推导式本质上是一系列低级的列表添加操作。这与上面描述的 PySequence_Tuple
中填充元组的方式类似。因此,从这个角度来看,这两种方法是相等的。然而,生成器表达式的区别在于它们需要额外的开销来创建一个生成器(一个包含多个 yield 的序列),这需要被迭代。因此,当使用列表推导式时,你可以避免这些额外的开销。
operator.itemgetter(你真的需要用2.5版本吗?这个版本已经过时了。)
除了更简单之外,它的速度也应该稍微快一点,因为它是用C语言实现的。你可以在知道想要哪些索引时,先构建一个itemgetter
对象,然后在很多列表上反复调用它。虽然每次还是需要复制N个项目并创建一个元组,但它会尽可能快地完成这些操作。