比较列表推导式和生成器表达式时出现意外结果。

8

我觉得我可能忽略了一些简单的东西,但是我似乎无法确切地弄清楚。请看下面的代码:

a = [2, 3, 4, 5]

lc = [ x for x in a if x >= 4 ] # List comprehension
lg = ( x for x in a if x >= 4 ) # Generator expression

a.extend([6,7,8,9])

for i in lc:
    print("{} ".format(i), end="")

for i in lg:
    print("{} ".format(i), end="")

我原本期望两个for循环会产生相同的结果,即 4 5。然而,打印生成器表达式的for循环会输出4 5 6 7 8 9。我认为这与列表推导的声明有关(在扩展之前声明)。但是为什么生成器的结果不同,因为它也是在扩展列表之前声明的呢?换句话说,内部发生了什么?


不太同意重复,虽然代码完全不同,但底层原理相同。 - Psychotechnopath
被接受的答案解释了这个问题https://dev59.com/kVgQ5IYBdhLWcg3wazMi#42806184,但我相信我们可以找到大量其他重复的目标。 - Jean-François Fabre
同样的问题:条件生成表达式出现意外行为 - Georgy
3个回答

4
生成器在调用next()时才会被评估,这使它们变得有用,而列表推导式则会立即被评估。
因此,在扩展之前lc = [4,5]已经完成。 lg在开始时仍然是相同的值,因此extend仍然适用于在生成器中尚未完成评估的a,这意味着在打印它之前a会被扩展,这就是为什么它会与其他数字一起打印出更长的原因。
像这样检查它:
>>> a = [2, 3, 4, 5]
>>> lg = ( x for x in a if x >= 4 )
>>> next(lg)
4
>>> next(lg)
5
>>> a.extend([6,7,8,9])
>>> next(lg)
6

然而,如果你在调用 extend() 之前尝试调用额外的 next(),你会收到 StopIteration 错误,因为此时生成器已经耗尽,你将无法再次调用它。

>>> a = [2, 3, 4, 5]
>>> lg = ( x for x in a if x >= 4 )
>>> next(lg)
4
>>> next(lg)
5
>>> next(lg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> a.extend([6,7,8,9])
>>> next(lg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

2

内部发生了什么?

生成器本质上是懒惰的。

[ x for x in a if x >= 4 ] 一旦执行,就会被立即评估。

( x for x in a if x >= 4 ) 在执行时只创建了生成器。循环本身仅在生成器以一种可能的方式(手动调用next、转换为另一种可迭代类型 [列表、元组、集合等] 或使用 for 循环)消耗时才被评估/执行。

生成器之所以具有懒加载的优势是因为它们不需要将所有元素存储在内存中,而只需要当前(或下一个)元素。


1
生成器表达式是惰性求值的,因此当您得到生成器对象时,代码 x for x in a if x >= 4 尚未执行。
for-in 循环在每次迭代该生成器对象时内部调用内置的 next() 函数。 next() 调用实际上评估了代码,并且该代码指向更新的 list,该列表具有在创建生成器对象后添加的新值集。
>>> lg = ( x for x in a if x >= 4)
#evaluates the code and returns the first value
>>> next(lg) 
4
>>> next(lg)
5
# if new values are added here to the list 
# the generator will return them

但是在列表推导式的情况下,生成器对象的next()方法会立即被调用,并且使用一开始存在的值将所有值添加到列表容器中。

内置的list()[]接受一个可迭代对象作为参数,并使用从可迭代对象返回的值构造列表。当您将可迭代对象(在您的情况下是可迭代的生成器对象)传递给列表构造函数时,这会立即发生。

但是另一方面,如果您只是执行生成器表达式,则会返回生成器对象,该对象只是可迭代对象,也是迭代器。因此,您需要在其上调用next()以执行代码并获取值,或者在for in iterable:循环中使用它,该循环会隐式地执行。

但请记住,一旦您通过获取StopIteration异常来耗尽生成器对象,并且您在列表中添加了一个新值,由于生成器对象只能被消耗一次,因此该值将不会从next()调用中返回。

>>> a = [2, 3, 4, 5]
>>> lg = ( x for x in a if x >= 4)
>>> next(lg)
4
>>> next(lg)
5
>>> a.append(9)
>>> next(lg)
9
>>> next(lg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
# lg is consumed
>>> a.append(10)
>>> next(lg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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