元组推导式和星号展开运算符*

6
我刚刚阅读了这个问题:为什么Python中没有元组推导?

被接受的答案的评论中,有人说目前没有真正的“元组推导”。相反,我们现在的选择是使用生成器表达式并将生成器对象传递给元组构造函数:
tuple(thing for thing in things)

另一种方法是使用列表推导式创建一个列表,然后将该列表传递给元组构造函数:

tuple([thing for thing in things])

最后,与被接受的回答相反,一篇较新的回答指出元组推导确实存在(自Python 3.5起),使用以下语法:
*(thing for thing in things),
  • 在我看来,第二个示例也是先创建了生成器对象。这是正确的吗?

  • 这些表达式在背后发生的情况上有什么区别吗?在性能方面呢?我认为第一个和第三个可能会有延迟问题,而第二个可能会有内存问题(如链接的评论中所讨论的那样)。

  • 比较第一个和最后一个,哪一个更符合Python风格?

更新:

正如预期的那样,列表推导确实要快得多。然而,我不明白为什么第一个比第三个更快。有什么想法吗?

>>> from timeit import timeit

>>> a = 'tuple(i for i in range(10000))'
>>> b = 'tuple([i for i in range(10000)])'
>>> c = '*(i for i in range(10000)),'

>>> print('A:', timeit(a, number=1000000))
>>> print('B:', timeit(b, number=1000000))
>>> print('C:', timeit(c, number=1000000))

A: 438.98362647295824
B: 271.7554752581845
C: 455.59842588083677

你关心性能问题。测试它们。在IPython中尝试%timeit。找出在你的特定机器上哪个更好。 - John Zwinck
列表推导式中的 x for y in z 看起来像是一个生成器,但实际上并不是。其内部机制是不同的。例如,在 x 部分引发的 StopIteration 会停止生成器,但会从列表推导式中冒出。 - user2390182
2
我认为两者都不是很符合Python的风格,因为元组通常用于表示静态已知、可能是异构项的集合(您可以对其进行解构),每个位置都与一些语义含义相关联。列表更适合于不确定的同质多元素,其中像迭代这样的操作是有意义的。不过这只是我的观点。 - Asad Saeeduddin
1
虽然 technically 最后一个选项可以被使用,但它是所有选项中最慢的。而且需要粘贴一些无关紧要的逗号只是为了让解释器能够理解它需要展开元组,在我看来这并不符合"Pythonic"的风格。 - Uvar
完成了!我更新了问题@JohnZwinck。 还有@schwobaseggl,我不确定我是否理解了,我使用的是x for x in y而不是x for y in z。 关于这里提出的其他观点,我同意你们所有人。 - Lucubrator
关于性能的问题已经在原始帖子中得到了解决,“Pythonicness”是主观的并且与主题无关,因此我将其作为重复内容关闭并返回到原始帖子。 - Karl Knechtel
1个回答

1
对我来说,第二个示例似乎也是首先创建生成器对象的示例。这是正确的吗?
是的,你是正确的,请检查CPython字节码:
>>> import dis
>>> dis.dis("*(thing for thing in thing),")
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x7f56e9347ed0, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<genexpr>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (thing)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 BUILD_TUPLE_UNPACK       1
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

这些表达式背后的运行方式有什么区别吗?性能方面呢?我认为第一种和第三种可能会有延迟问题,而第二种可能会有内存问题(正如链接评论中所讨论的)。我的时间表明第一种稍微快一点,可能是因为通过BUILD_TUPLE_UNPACK进行解包比tuple()调用更加昂贵。
>>> from timeit import timeit
>>> def f1(): tuple(thing for thing in range(100000))
... 
>>> def f2(): *(thing for thing in range(100000)),
... 
>>> timeit(lambda: f1(), number=100)
0.5535585517063737
>>> timeit(lambda: f2(), number=100)
0.6043887557461858

比较第一个和最后一个,哪个更符合Python风格?
对我来说,第一个更易读,而且也适用于不同的Python版本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接