Python 3中的列表推导式是用来简化`list(generator expression)`语法的吗?

45
在Python 3中,列表推导式是否只是一种语法糖,将生成器表达式传递给list函数?
例如,下面的代码是否等价:
squares = [x**2 for x in range(1000)]

实际上在后台转换成以下内容?

squares = list(x**2 for x in range(1000))

我知道输出结果是相同的,Python 3修复了列表推导式对于周围命名空间的惊人副作用,但就CPython解释器在底层执行代码方面而言,前者是否被转换为后者,或者代码的执行方式是否有任何不同?

背景

我在这个问题的评论部分发现了这个等价性的说法,并且快速的谷歌搜索显示这里也做了同样的说法。

Python 3.0文档更新中也提到了这个问题,但措辞有些模糊:

此外,请注意,列表推导式具有不同的语义:它们更接近于在list()构造函数内部的生成器表达式的语法糖,特别是循环控制变量不再泄露到周围作用域中。


1
我不确定这个“假设”是否正确。据我所知,列表推导式是for循环的语法糖,而生成器表达式具有非常不同的语义——即它可以迭代“生成”值。耸了耸肩,也许在Python 3中语义已经改变了:) - James Mills
1
注意:确切的措辞是“更接近于在list()构造函数中使用生成器表达式”,而不是它们确切地是那样。 - James Mills
1
@JamesMills 谢谢,没错,确切地说,“更接近语法糖”这一点让我有些困惑。我的意思是,到底有多接近?它明确地不是语法糖吗? - zehnpaard
3
如果有疑问,请使用 dis 模块进行检查。 - Karl Knechtel
4个回答

40

两者工作方式不同。列表推导式版本利用了特殊的字节码LIST_APPEND,直接为我们调用PyList_Append。因此它避免了对list.append的属性查找和在Python层面上的函数调用。

>>> def func_lc():
    [x**2 for x in y]
...
>>> dis.dis(func_lc)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10d3c6780, file "<ipython-input-42-ead395105775>", line 2>)
              3 LOAD_CONST               2 ('func_lc.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              0 (y)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 POP_TOP
             17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

>>> lc_object = list(dis.get_instructions(func_lc))[0].argval
>>> lc_object
<code object <listcomp> at 0x10d3c6780, file "<ipython-input-42-ead395105775>", line 2>
>>> dis.dis(lc_object)
  2           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                16 (to 25)
              9 STORE_FAST               1 (x)
             12 LOAD_FAST                1 (x)
             15 LOAD_CONST               0 (2)
             18 BINARY_POWER
             19 LIST_APPEND              2
             22 JUMP_ABSOLUTE            6
        >>   25 RETURN_VALUE

另一方面,list() 版本仅将生成器对象传递给列表的 __init__ 方法,然后在内部调用其 extend 方法。由于对象不是列表或元组,CPython 首先获取其迭代器,然后将项目添加到列表中,直到迭代器耗尽为止:

>>> def func_ge():
    list(x**2 for x in y)
...
>>> dis.dis(func_ge)
  2           0 LOAD_GLOBAL              0 (list)
              3 LOAD_CONST               1 (<code object <genexpr> at 0x10cde6ae0, file "<ipython-input-41-f9a53483f10a>", line 2>)
              6 LOAD_CONST               2 ('func_ge.<locals>.<genexpr>')
              9 MAKE_FUNCTION            0
             12 LOAD_GLOBAL              1 (y)
             15 GET_ITER
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE
>>> ge_object = list(dis.get_instructions(func_ge))[1].argval
>>> ge_object
<code object <genexpr> at 0x10cde6ae0, file "<ipython-input-41-f9a53483f10a>", line 2>
>>> dis.dis(ge_object)
  2           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                15 (to 21)
              6 STORE_FAST               1 (x)
              9 LOAD_FAST                1 (x)
             12 LOAD_CONST               0 (2)
             15 BINARY_POWER
             16 YIELD_VALUE
             17 POP_TOP
             18 JUMP_ABSOLUTE            3
        >>   21 LOAD_CONST               1 (None)
             24 RETURN_VALUE
>>>

时间比较:

>>> %timeit [x**2 for x in range(10**6)]
1 loops, best of 3: 453 ms per loop
>>> %timeit list(x**2 for x in range(10**6))
1 loops, best of 3: 478 ms per loop
>>> %%timeit
out = []
for x in range(10**6):
    out.append(x**2)
...
1 loops, best of 3: 510 ms per loop

普通循环由于属性查找缓慢而稍微慢一些。进行缓存,然后再次计时。

>>> %%timeit
out = [];append=out.append
for x in range(10**6):
    append(x**2)
...
1 loops, best of 3: 467 ms per loop

除了列表推导式不再泄漏变量之外,另一个区别是像这样的代码现在不再有效:

>>> [x**2 for x in 1, 2, 3] # Python 2
[1, 4, 9]
>>> [x**2 for x in 1, 2, 3] # Python 3
  File "<ipython-input-69-bea9540dd1d6>", line 1
    [x**2 for x in 1, 2, 3]
                    ^
SyntaxError: invalid syntax

>>> [x**2 for x in (1, 2, 3)] # Add parenthesis
[1, 4, 9]
>>> for x in 1, 2, 3: # Python 3: For normal loops it still works
    print(x**2)
...
1
4
9

感谢您提供详细的回复!有趣的是,尽管列表理解和生成器表达式的底层字节码(和C代码)非常不同,但timeit显示它们被抛入“list”中时之间差异可以忽略不计。 - zehnpaard
10
引用Guido的帖子(http://python-history.blogspot.in/2010/06/from-list-comprehensions-to-generator.html):“在你开始担心Python 3中列表推导变慢之前,请注意:由于Python 3进行了大量的实现工作以提高总体速度,因此Python 3中的列表推导和生成器表达式实际上比Python 2中快!(而且两者之间不再有速度差异。)” - Ashwini Chaudhary
你如何在Python shell中直接使用百分号运行timeit? - Zaar Hai
2
@ZaarHai 这是使用 --classic 参数运行的 ipython shell。 - Ashwini Chaudhary
2
@zehnpaard 差距已不容忽视 - 第一个版本大约快了30%,详见 https://stackoverflow.com/q/52053579/5769463 - ead
只是一则说明:从Python 3.11开始,属性查找不再减慢正常循环的速度,因此append方法缓存技巧不再有用。相关讨论:https://discuss.python.org/t/append-mylist-append-optimization-now-harmful/36028以及性能比较:https://pym.dev/p/2gzr3/ - undefined

15

两种形式都创建并调用匿名函数。然而,list(...) 形式创建一个生成器函数,并将返回的生成器迭代器传递给 list,而使用 [...] 形式时,匿名函数通过 LIST_APPEND 操作码直接构建列表。

下面的代码获取了一个示例理解表达式及其相应的传递给 list 的生成器的反编译输出:

import dis

def f():
    [x for x in []]

def g():
    list(x for x in [])

dis.dis(f.__code__.co_consts[1])
dis.dis(g.__code__.co_consts[1])

列表推导式的输出结果为

  4           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                12 (to 21)
              9 STORE_FAST               1 (x)
             12 LOAD_FAST                1 (x)
             15 LIST_APPEND              2
             18 JUMP_ABSOLUTE            6
        >>   21 RETURN_VALUE

生成器表达式的输出结果为

  7           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (x)
              9 LOAD_FAST                1 (x)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

请注意,名称 list 也需要显式地查找,因为它可能已被遮蔽。 - Karl Knechtel
1
谢谢您,dis模块总是很有见地,但有时也会让人感到困惑。在您的生成器表达式示例中,如果我理解正确,列表创建似乎完全被省略了,但我不知道为什么... - zehnpaard
1
@zehnpaard:那不是匿名函数的一部分;list调用处理了那个。 - user2357112
1
好的,g.__code__.co_consts[1] 具体指向匿名函数吗? - zehnpaard
1
@zehnpaard:它指向用于构建匿名函数的代码对象。 - user2357112

5

您可以通过展示两者的不同结果来证明它们本质上是不同的:

>>> list(next(iter([])) if x > 3 else x for x in range(10))
[0, 1, 2, 3]

>>> [next(iter([])) if x > 3 else x for x in range(10)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
StopIteration

因为推导式没有处理StopIteration,所以推导式中的表达式不被视为生成器,而list构造函数则会处理。


6
注意,在Python 3.7/3.8中,顶部的代码会引发 RuntimeError: generator raised StopIteration 错误,请参考 https://www.python.org/dev/peps/pep-0479/。 - Chris_Rands

-3

它们并不相同,list()会在括号内的内容执行完毕后再进行评估,而不是之前。

在Python中,[]有点神奇,它告诉Python将括号内的任何内容作为列表进行包装,更像是语言的类型提示。


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