1. FlyPython首页
  2. Python高级话题

如何在 Python 中展平列表

如何在 Python 中展平列表

不知怎么的,你最终得到了嵌套在列表中的列表,可能像这样:

>>> groups = [["Hong", "Ryan"], ["Anthony", "Wilhelmina"], ["Margaret", "Adrian"]]

但是您只需要一个列表(不带嵌套) ,如下所示:

>>> expected_output = ["Hong", "Ryan", "Anthony", "Wilhelmina", "Margaret", "Adrian"]

你需要平展你的列表。

我们要找的是一个“浅层”扁平化

我们可以认为这是一个浅层的扁平化操作,这意味着我们将这个列表扁平化了一个级别。Deep flatten 操作将处理列表列表(lists-of-lists-of-lists)(等等) ,这比我们的用例所需要的要多一点。

我们提出的扁平化策略应该适用于列表列表以及任何其他类型的迭代对象。例如,元组列表应该是可展开的:

>>> groups = [("Hong", "Ryan"), ("Anthony", "Wilhelmina"), ("Margaret", "Adrian")]

甚至像 dict _ items 对象这样的奇数类型(我们通过查询字典获得它的条目)应该是扁平的:

>>> fruit_counts = {"apple": 3, "lime": 2, "watermelon": 1, "mandarin": 4}
>>> fruit_counts.items()
dict_items([('apple', 3), ('lime', 2), ('watermelon', 1), ('mandarin', 4)])
>>> flattened_counts = ['apple', 3, 'lime', 2, 'watermelon', 1, 'mandarin', 4]

用 for 循环平坦迭代器

使迭代可迭代对象变平的一种方法是使用 for 循环。我们可以循环一个级别的深度来获得每个内部迭代。

for group in groups:
    ...

然后我们循环第二级深度,从每个内部迭代中获取每个项。

for group in groups:
    for name in group:
        ...

然后将每个项目附加到一个新列表中:

names = []
for group in groups:
    for name in group:
        names.append(name)

还有一个列表方法可以缩短这个时间,那就是 extend 方法:

names = []
for group in groups:
    names.extend(group)

List extend 方法接受一个可迭代的项,并将每个项追加到您提供给它的可迭代项中。

或者我们可以使用 + = 运算符将每个列表连接到我们的新列表:

names = []
for group in groups:
    names += group

您可以将列表中的 + = 看作是调用 extend 方法。对于列表,这两个操作(+ = 和 extend)是等价的。

通过一个 comprehension 实现迭代的展开

这个嵌套的 for 循环加上一个追加调用看起来很熟悉:

names = []
for group in groups:
    for name in group:
        names.append(name)

这段代码的结构看起来像是我们可以复制粘贴到列表内涵文件夹中的东西。

在方括号内,我们先复制要附加的东西,然后是第一个循环的逻辑,然后是第二个循环的逻辑:

names = [
    name
    for group in groups
    for name in group
]

这个理解循环深入到两个层次,就像我们嵌套的 for 循环一样。注意,comprehension 中 for 子句的顺序必须与 for 循环的顺序相同。

这些从句的顺序(有时令人困惑)是我建议将复制粘贴到理解中的部分原因。当将 for 循环转换为 comprehension 时,for 和 if 子句保持在相同的关系位置,但是附加的内容从结尾移动到开始。

我们能在理解中用 * 变平吗?

但是 Python 的 * 操作符呢? 我已经介绍了 Python 中前缀星号符号的许多用法。

我们可以在 Python 的 list literal 语法([ … ])中使用 * 来将一个迭代解压缩到一个新列表中:

>>> numbers = [3, 4, 7]
>>> more_numbers = [2, 1, *numbers, 11, 18]
>>> more_numbers
[2, 1, 3, 4, 7, 11, 18]

我们可以使用 * 运算符来解压一个在理解中的迭代吗?

names = [
    *group
    for group in groups
]

我们不能。如果我们尝试这么做,Python 会明确告诉我们 * 操作符不能像下面这样用于 comprehension:

>>> names = [
...     *group
...     for group in groups
... ]
  File "<stdin>", line 2
    ]
     ^
SyntaxError: iterable unpacking cannot be used in comprehension

由于可读性方面的考虑,在 Python Enhancement Proposal 中将这个 *-in-list-literal 语法添加到 Python 中的 PEP 448特别排除了这个特性。

我们不能用求和吗?

下面是另一个我见过几次的使列表变平的技巧:

>>> names = sum(groups, [])

这个方法确实有效:

>>> names
['Hong', 'Ryan', 'Anthony', 'Wilhelmina', 'Margaret', 'Adrian']

但是我发现这个技巧很不直观。

我们在 Python 中使用 + 运算符来添加数字和连接序列,而 sum 函数恰好可以用于任何支持 + 运算符的操作(这要感谢 duck typing)。但在我看来,“和”这个词意味着算术: 把数字相加。

我发现“总和”列表让人困惑,所以我不推荐这种方法。

快速旁白: 算法和使用也使得列表展开速度非常慢(这里的时间比较)。在 Big-O 术语中(对于时间复杂度极低的人来说) ,有列表的和是 o (n * * 2)而不是 o (n)。

那么 itertools.chain 呢?

还有一个工具经常用于扁平化: 迭代工具模块中的链式工具。

Chain 接受任何数字参数,并返回一个迭代器:

>>> from itertools import chain
>>> chain(*groups)
<itertools.chain object at 0x7fc1b2d65bb0>

我们可以循环遍历这个迭代器,或者把它变成另一个迭代器,比如一个列表:

>>> list(chain(*groups))
['Hong', 'Ryan', 'Anthony', 'Wilhelmina', 'Margaret', 'Adrian']

链上实际上有一个方法专门用来展开一个单一的迭代:

>>> list(chain.from_iterable(groups))
['Hong', 'Ryan', 'Anthony', 'Wilhelmina', 'Margaret', 'Adrian']

使用 chain.from _ iterable 比使用 chain with * 更加高效,因为 * 在调用 chain 时立即展开整个迭代。

Recap: comparing list flattening techniques

如果你想懒惰地平滑一个可迭代的迭代对象,我会使用 itertools.chain.from _ iterable:

>>> from itertools import chain
>>> flattened = chain.from_iterable(groups)

这将返回一个迭代器,这意味着在返回的迭代器循环之前不会执行任何工作:

>>> list(flattened)
['Hong', 'Ryan', 'Anthony', 'Wilhelmina', 'Margaret', 'Adrian']

当我们循环时,它会被消耗掉,所以循环两次将导致一个空的迭代:

如果你发现 itertools.chain 有点太神秘,你可能更喜欢一个 for 循环,它调用一个新列表上的 extend 方法来重复扩展每个迭代中的值:

names = []
for group in groups:
    names.extend(group)

或者使用新列表中的 + = 操作符的 for 循环:

names = []
for group in groups:
    names += group

与 chain.from _ iterable 不同,这两个 for 循环构建的都是新列表,而不是惰性迭代器对象。

如果你认为列表理解是可读的(我喜欢它们发出“看,我们正在建立一个列表”的信号) ,那么你可能更喜欢理解:

names = [
    name
    for group in groups
    for name in group
]

如果你确实想要惰性(一个迭代器) ,但是你不喜欢 itertools.chain,你可以做一个生成器表达式,它和 itertools.chain.from _ iterable 一样:

names = (
    name
    for group in groups
    for name in group
)

快乐的列表变平了!

原创文章,作者:flypython,如若转载,请注明出处:http://flypython.com/advanced-python/573.html