理解Python中列表推导式扁平化列表的用法
我发现了一个很棒的写法,可以把一个列表里的列表变成一个平坦的列表:
>>> list_of_lists = [(1,2,3),(2,3,4),(3,4,5)]
>>> [item for sublist in list_of_lists for item in sublist]
[1, 2, 3, 2, 3, 4, 3, 4, 5]
我觉得这个方法比用 itertools.chain()
更好,但我就是搞不懂它。我试着在某些部分加上括号,想看看能不能简化一下,但现在我反而更困惑了:
>>> [(item for sublist in list_of_lists) for item in sublist]
[<generator object <genexpr> at 0x7ff919fdfd20>, <generator object <genexpr> at 0x7ff919fdfd70>, <generator object <genexpr> at 0x7ff919fdfdc0>]
>>> [item for sublist in (list_of_lists for item in sublist)]
[5, 5, 5]
我感觉我理解起来很困难,因为我对生成器的工作原理不是很清楚……我本以为我懂,但现在我真的开始怀疑了。就像我说的,我喜欢这个写法的简洁,它正是我需要的,但我不想使用我不理解的代码。
这里到底发生了什么呢?
3 个回答
列表推导式的工作原理是这样的:
[<what i want> <for loops in the order you'd write them naturally>]
在这个例子中,<我想要的东西>
是每个 item
在每个 子列表
中。要获取这些项目,你只需遍历原始列表中的子列表,然后保存或输出每个子列表中的每个项目。因此,列表推导式中的循环顺序和如果你不使用列表推导式时的顺序是一样的。唯一让人困惑的地方是,<我想要的东西>
是放在最前面的,而不是放在最后一个循环的内部。
把 for 循环看作是嵌套的,从左到右阅读。左边的表达式是用来生成最终列表中每个值的:
for sublist in list_of_lists:
for item in sublist:
item # added to the list
列表推导式也支持 if
测试来过滤使用哪些元素;这些也可以看作是嵌套的语句,就像 for
循环一样。
通过添加括号,你改变了表达式;括号里的内容现在是要添加的左侧表达式:
for item in sublist:
(item for sublist in list_of_lists) # added to the list
像这样的 for
循环是生成器表达式。它的工作方式和列表推导式完全一样,只是它不构建一个列表。元素是按需生成的。你可以请求生成器表达式下一个值,然后下一个值,依此类推。
在这种情况下,必须有一个已经存在的 sublist
对象才能正常工作;毕竟,外层循环不再是遍历 list_of_lists
。
你最后的尝试翻译成:
for sublist in (list_of_lists for item in sublist):
item # added to the list
这里 list_of_lists
是生成器表达式中的一个循环元素,它在 for item in sublist
中循环。再次强调,sublist
必须已经存在才能正常工作。循环然后将一个已经存在的 item
添加到最终的列表输出中。
在你的例子中,显然 sublist
是一个包含3个元素的列表;你最终生成的列表有3个元素。item
被绑定为 5
,所以你在输出中得到了3个 5
。
列表推导式
当我第一次接触列表推导式时,我把它们看成英语句子,这样我就能很容易理解它们。例如,
[item for sublist in list_of_lists for item in sublist]
可以理解为
for each sublist in list_of_lists and for each item in sublist add item
另外,过滤的部分可以理解为
for each sublist in list_of_lists and for each item in sublist add item only if it is valid
对应的推导式就是
[item for sublist in list_of_lists for item in sublist if valid(item)]
生成器
生成器就像地雷,只有在用 next
这个方法调用时才会被触发。它们和函数有点像,但在抛出异常或者函数结束之前,生成器不会被耗尽,可以一次又一次地调用。重要的是,它们在上一次调用和当前调用之间会保留状态。
生成器和函数的区别在于,生成器使用 yield
这个关键字把值返回给调用者。如果是生成器表达式,它们和列表推导式类似,最开始的表达式就是实际被“产生”的值。
有了这个基本理解,如果我们看看你问题中的表达式,
[(item for sublist in list_of_lists) for item in sublist]
你把列表推导式和生成器表达式混在一起了。这可以理解为
for each item in sublist add a generator expression which is defined as, for every sublist in list_of_lists yield item
这并不是你想要的效果。而且因为生成器表达式没有被迭代,所以生成器表达式对象会原封不动地加到列表里。由于它们在没有用下一个方法调用的情况下不会被计算,所以不会产生任何错误(如果有错误,除非是语法错误)。在这种情况下,会产生运行时错误,因为 sublist
还没有定义。
另外,在最后一种情况下,
[item for sublist in (list_of_lists for item in sublist)]
for each sublist in the generator expression, add item and the generator expression is defined as for each item in sublist yield list_of_lists.
for 循环会用下一个方法迭代任何可迭代的对象。所以,生成器表达式会被计算,item
将始终是 sublist
迭代中的最后一个元素,而你把它加到了列表里。这也会产生运行时错误,因为 sublist
还没有定义。