如何压缩python列表的列表理解工作?

How does the list comprehension to flatten a python list work?

我最近寻找一种方法将嵌套的python列表扁平化,如:【1,2,3】、【4,5,6】;到这个列表中:【1,2,3,4,5,6】。

StackOverflow和以前一样有用,我找到了一篇具有这种巧妙列表理解能力的文章:

1
2
l = [[1,2,3],[4,5,6]]
flattened_l = [item for sublist in l for item in sublist]

我以为我理解了列表理解是如何工作的,但显然我还没有一个最模糊的概念。最让我困惑的是,除了上述的理解之外,这也同样存在(尽管它没有给出相同的结果):

1
exactly_the_same_as_l = [item for item in sublist for sublist in l]

有人能解释一下python是如何解释这些东西的吗?基于第二个压缩,我希望Python能够将其解释回前面,但显然情况并非总是如此。如果是这样,第一次理解应该抛出一个错误,因为"子列表"不存在。我的思想完全扭曲了,救命!


让我们看看你的列表理解,但首先让我们从列表理解开始,它是最简单的。

1
2
l = [1,2,3,4,5]
print [x for x in l] # prints [1, 2, 3, 4, 5]

您可以将其与这样的for循环结构相同:

1
2
for x in l:
    print x

现在让我们来看另一个:

1
2
3
l = [1,2,3,4,5]
a = [x for x in l if x % 2 == 0]
print a # prints [2,4]

与此完全相同:

1
2
3
4
5
6
a = []
l = [1,2,3,4,5]
for x in l:
    if x % 2 == 0:
        a.append(x)
print a # prints [2,4]

现在让我们来看一下您提供的示例。

1
2
3
l = [[1,2,3],[4,5,6]]
flattened_l = [item for sublist in l for item in sublist]
print flattened_l # prints [1,2,3,4,5,6]

对于列表理解,从最左边的for循环开始,并按您的方式进行。在这种情况下,变量item将被添加。它将产生这种等效物:

1
2
3
4
5
l = [[1,2,3],[4,5,6]]
flattened_l = []
for sublist in l:
    for item in sublist:
        flattened_l.append(item)

现在是最后一个了

1
exactly_the_same_as_l = [item for item in sublist for sublist in l]

使用相同的知识,我们可以创建一个for循环,并查看它的行为

1
2
3
for item in sublist:
    for sublist in l:
        exactly_the_same_as_l.append(item)

现在唯一的原因是上面的一个作品是因为当扁平的"l"被创造出来的时候,它也创造了"EDOCX1"(1)。这是不引发错误的一个范围界定原因。如果你不先定义扁平的,你会得到一个NameError


for循环从左到右进行计算。任何list comprehension都可以重写为for循环,如下所示:

1
2
3
4
5
l = [[1,2,3],[4,5,6]]
flattened_l = []
for sublist in l:
    for item in sublist:
        flattened_l.append(item)

以上是压平列表的正确代码,无论您是选择以列表理解的形式简洁地编写列表,还是在此扩展版本中编写列表。

由于尚未定义"子列表",您编写的第二个列表理解将引发一个名称错误。您可以通过将列表理解写成for循环来看到这一点:

1
2
3
4
5
l = [[1,2,3],[4,5,6]]
flattened_l = []
for item in sublist:
    for sublist in l:
        flattened_l.append(item)

运行代码时没有看到错误的唯一原因是,在实现第一个列表理解时,您以前定义了子列表。

有关更多信息,您可以查看guido关于列表理解的教程。


对于需要快速回答的懒惰开发人员:

1
2
3
>>> a = [[1,2], [3,4]]
>>> [i for g in a for i in g]
[1, 2, 3, 4]


虽然这种方法确实适用于扁平化列表,但我不建议这样做,除非知道子列表非常小(每个子列表有1或2个元素)。

我对timeit做了一些分析,发现这比使用单个循环和调用extend所花费的时间大约长2-3倍。

1
2
3
4
5
def flatten(l):
    flattened = []
    for sublist in l:
        flattened.extend(sublist)
    return flattened

虽然不是很漂亮,但加速很重要。我认为这样做很好,因为extend可以更有效地一次复制整个子列表,而不是一次复制每个元素。如果你知道你的子列表是大到中的,我建议你使用extend。子列表越大,速度越快。

最后一个警告:很明显,这只有在您需要急切地形成这个扁平的列表时才成立。例如,您可能稍后会对其进行排序。如果您最终只按原样遍历列表,那么这不会比使用其他人概述的嵌套循环方法更好。但是对于这个用例,为了增加懒惰的好处,您希望返回一个生成器而不是一个列表…

1
2
def flatten(l):
    return (item for sublist in l for item in sublist) # note the parens


当然,请注意,这种理解只会"压扁"列表(或其他iterables的列表)。另外,如果你给它一个字符串列表,你会把它"展平"成一个字符列表。

要以有意义的方式概括这一点,首先您希望能够清楚地区分字符串(或bytearray)和其他类型的序列(或其他iterables)。那么让我们从一个简单的函数开始:

1
2
3
4
import collections
def non_str_seq(p):
    '''p is putatively a sequence and not a string nor bytearray'''
    return isinstance(p, collections.Iterable) and not (isinstance(p, str) or isinstance(p, bytearray))

使用它,我们可以构建一个递归函数来扁平任何

1
2
3
4
5
6
7
8
9
10
def flatten(s):
    '''Recursively flatten any sequence of objects
    '''

    results = list()
    if non_str_seq(s):
        for each in s:
            results.extend(flatten(each))
    else:
        results.append(s)
    return results

可能有更优雅的方式来做到这一点。但这适用于我所知道的所有Python内置类型。简单对象(数字、字符串、none、true、false的实例都将被包装在列表中返回。字典作为键列表返回(按哈希顺序)。