【问题标题】:Tuple() on GenExp vs. ListCompGenExp 与 ListComp 上的 Tuple()
【发布时间】:2014-04-25 15:50:21
【问题描述】:

我有一些(少量)项目的列表,例如:

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

这是异常情况,还是列表理解真的比生成器表达式好得多?

【问题讨论】:

  • 您正在寻找的输出究竟是什么?我不完全理解你的问题。
  • @sdamashek 他在问为什么在 genexp 上调用 tuple 比在列表 comp 上调用 tuple 慢得多
  • 我也在问是否有更好/更快的方法来实现同样的目标。理想情况下,我想说:tuple(my_list[indexes])
  • @delnan - 有趣,我从来不知道。我将删除我的其他评论,因为它显然是错误的。感谢您的信息!

标签: python performance list tuples


【解决方案1】:

operator.itemgetter(真的要用2.5吗?死了埋了。)

除了更简单之外,由于是用 C 实现的,它也应该稍微快一些。当你知道你想要哪些索引时,你可以构造一个 itemgetter 项目,然后在许多列表上重复调用它。它仍然需要每次复制 N 项并创建一个元组,但它应该尽可能快地完成此操作。

【讨论】:

  • +1 很好,没想到。而且速度真的非常快!
  • 这并不是问题的真正答案,而是一个不错的解决方案!
  • @dorvak 我完全不同意。 OP 想知道如何更快地实现它,delnan 给了他一种更快地实现它的方法。这纯粹是对问题的回答
  • operator.itemgetter() 的隐藏 2.5 文档链接。请叫我掘墓人。
  • @Adam Smith Sry,完全错过了那部分!你是对的,这个答案也是如此;)
【解决方案2】:

元组是一个不可变的序列,所以当它被创建(和它的内存分配)时,它确实需要首先知道它将包含多少元素。这意味着从生成器表达式创建元组时,必须首先完全迭代生成器——因为生成器只能被使用一次——并且元素需要存储在某个地方。因此,正在发生的事情可以与此进行比较:

tuple(list(generator))

现在,从生成器表达式创建列表比使用列表推导式创建列表要慢,因此您只需使用列表推导式创建列表即可节省时间。

如果您没有真正的理由使用元组,即如果您不需要不变性,您也可以保留列表而不将其转换为元组以节省更多时间。

最后,不,没有比迭代索引并查询每个索引的序列更好的方法了。即使索引一直相同,它们仍然需要针对每个列表进行评估,所以无论如何你都必须重复它。

但是,如果这些索引实际上是固定的,您可以节省更多时间。因为一个简单的(l[1], l[5], l[9]) 会比其他任何东西都快得多;)


这里有一些来自源的引用(这里使用3.4,但在2.x中应该类似):

使用内置tuple() 函数创建元组在函数PySequence_Tuple 中完成。

如果参数是一个列表,那么 Python 将通过调用 PyList_AsTuple 来显式处理它,这实际上分配了一个列表长度的元组,然后只是复制所有项目。

否则,它将根据参数创建一个迭代器并首先尝试猜测长度。由于生成器没有长度,Python 将使用默认猜测 10 并分配该长度的元组 - 请注意,对于您的元组,我们分配的 7 个空格太多了。然后它将迭代迭代器并将每个值分配给它在元组中的位置。之后会resize the created tuple

现在,实际差异可能在于列表推导的工作方式。列表推导本质上是一系列低级列表追加。因此,它的工作方式类似于在PySequence_Tuple 中填充元组的方式,如上所述。因此,这两种方法都是平等的。但是,生成器表达式的不同之处在于,它们具有实际创建需要迭代的生成器(一系列收益)的开销。因此,所有这些都是您在进行列表理解时避免的额外内容。

【讨论】:

  • “它确实需要先知道它将包含多少元素”+1 才能触及性能问题的核心!
  • 不幸的是,这也是错误的。 PySequence_Tupletuple(iterable) 重定向到)单次通过可迭代对象。它从猜测长度开始,当结果太低时,它使用与列表相同的重新分配策略(甚至更激进=更快),但使用元组的内部缓冲区。最后,它返回分配的任何多余空间,可能相对于列表有一些开销,但它不会迭代两次,也不会在创建元组之前创建所有元素的列表。见hg.python.org/cpython/file/2.5/Objects/abstract.c#l1479
  • @delnan 我并不是说它等同于tuple(list(generator)),但可以比较,因为在迭代期间,元素需要存储在某个地方。碰巧的是 Python 保持它创建的元组在那个时候是“可变的”,所以不需要单独的列表。我已经调整了句子以使其更加清晰;我还从源代码中添加了更多解释。
  • 好。但是这种差异使得可疑开销(复制/迭代两次)的主要来源消失了。确实还有其他减速的来源,但我认为任何涉及它们的答案都应该非常清楚,以避免误解。特别是,它应该提到你在附录中描述的工作的哪些部分也会发生在列表理解中。
【解决方案3】:

另一种选择,虽然比 delnan 慢,但将__getitem__map 结合使用。但是,即使使用 import 语句,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-variant 比 itemgetter-variant 慢:

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

【讨论】:

  • 使用map__getitem__ 也比列表理解解决方案慢;它在 Python 2 中的工作方式也非常相似(map 创建一个列表,并且在评估列表推导时也会在内部调用__getitem__)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-12
  • 1970-01-01
  • 2013-10-26
  • 2018-01-25
  • 1970-01-01
  • 1970-01-01
  • 2019-12-02
相关资源
最近更新 更多