列表推导式中的双重迭代

389

在Python中,您可以在列表推导式中拥有多个迭代器,如下所示:

[(x,y) for x in a for y in b]

对于一些适当的序列a和b。我知道Python列表推导式的嵌套循环语义。

我的问题是:在推导式中,一个迭代器是否可以引用另一个?换句话说,我能不能像这样写:

[x for x in a for a in b]

外层循环的当前值是内层迭代器吗?

举个例子,如果我有一个嵌套列表:

a=[[1,2],[3,4]]

如何使用列表推导式得到这个结果:

[1,2,3,4]

请只列出列表推导式的答案,因为这正是我想找到的。


4
与其在评论区争论,我选择在 Meta 上开了一个讨论 - Karl Knechtel
11个回答

408
假设你有一段充满句子的文本,你想要一个单词数组。

假设您有一段充满句子的文本,并且您想要一个单词数组。

# Without list comprehension
list_of_words = []
for sentence in text:
    for word in sentence:
       list_of_words.append(word)
return list_of_words

我喜欢把列表推导式看作是将代码横向拉伸。

试着把它分解成:

# List Comprehension 
[word for sentence in text for word in sentence]

例子:

>>> text = (("Hi", "Steve!"), ("What's", "up?"))
>>> [word for sentence in text for word in sentence]
['Hi', 'Steve!', "What's", 'up?']

这也适用于生成器

>>> text = (("Hi", "Steve!"), ("What's", "up?"))
>>> gen = (word for sentence in text for word in sentence)
>>> for word in gen: print(word)
Hi
Steve!
What's
up?

38
计算机科学中只有两个难题:缓存失效和命名事物。-- Phil Karlton - cezar
2
这是一个很棒的答案,因为它让整个问题变得不那么抽象!谢谢! - A. Blesius
2
你可以选择另一种方式:[[word for word in sentence] for sentence in text] - Uylenburgh
2
@Saskia 不完全正确。这只会将相同的输入返回给你。你明白为什么吗? - Skam
4
我更喜欢这个顺序:[letter for letter in word for word in sentence for sentence in text] - Jeyekomon
显示剩余5条评论

200

按照你自己的建议回答你的问题:

>>> [x for b in a for x in b] # Works fine

虽然你要求的是列表推导式,但我也想指出优秀的itertools.chain()函数:

>>> from itertools import chain
>>> list(chain.from_iterable(a))
>>> list(chain(*a)) # If you're using python < 2.6

49
这段代码是Python中的列表推导式,意思是将二维列表a中的所有元素取出来放到一个新列表中。在Python中,x for x in y是常见的列表推导式形式,其中for后面跟着的变量直接影响了左侧表达式的结果。但是当你使用双重推导式时,最近一次迭代的变量突然变得"遥远",这样的语法让人感到很尴尬,不自然。 - Cruncher
1
更糟糕的是:它混淆了前后顺序。这就像写07/04/1776来表示1776年第七个月的第四天一样荒谬。 - user0

160

哎呀,我想我找到答案了:我没有足够注意哪个循环是内部循环,哪个是外部循环。列表推导式应该像这样:

[x for b in a for x in b]

为了得到所需的结果,是的,一个当前值可以作为下一个循环的迭代器。


91
列表推导式语法并不是 Python 的亮点之一。 - Glenn Maynard
4
@Glenn 是的,对于不仅仅是简单表达式的内容容易变得复杂。 - ThomasH
1
呃,我不确定这是否是列表推导式的“通常”用法,但在Python中链接起来非常麻烦,这真是太不幸了。 - Matt Joiner
20
如果在每个 'for' 前放置换行符,看起来会更加清晰。 - Nick Garvey
31
哇,这完全与我的理解相反。 - obskyr
你能给出使用案例的例子吗? - user305883

71

迭代器的顺序可能看起来有些反直觉。

[str(x) for i in range(3) for x in foo(i)]为例:

让我们对其进行分解:

def foo(i):
    return i, i + 0.5

[str(x)
    for i in range(3)
        for x in foo(i)
]

# is same as
for i in range(3):
    for x in foo(i):
        yield str(x)

9
真是大开眼界!! - nehem
2
我理解的原因是,“第一个迭代被列出的是如果列表推导式被写成嵌套的for循环,最上层的迭代”。这个原因令人感到不直观的是,最外层循环(如果按照嵌套的for循环来理解,则最上层)出现在括号列表/字典(推导式对象)的内部。相反,最内层循环(如果按照嵌套的for循环来理解,则最内层)恰好是列表推导式中最右侧的循环,并以这种方式出现在推导式的外部。 - Zach Siegel
2
抽象地写,我们有 [(loop 2 中的输出) (loop 1) (loop 2)],其中 (loop 1) = for i in range(3)(loop 2) = for x in foo(i):,且 (loop 2 中的输出) = str(x) - Qaswed

27

这个记忆技巧对我帮助很大:

[ <返回值> <外循环1> <内循环2> <内循环3> ... <可选条件> ]

现在你可以将 返回值外循环 视为唯一的 顺序

了解了上述内容,即使是三重循环的列表推导式看起来也很容易:


c=[111, 222, 333]
b=[11, 22, 33]
a=[1, 2, 3]

print(
  [
    (i, j, k)                            # <RETURNED_VALUE> 
    for i in a for j in b for k in c     # in order: loop1, loop2, loop3
    if i < 2 and j < 20 and k < 200      # <OPTIONAL_IF>
  ]
)
[(1, 11, 111)]
因为上面只是一个:
for i in a:                         # outer loop1 GOES SECOND
  for j in b:                       # inner loop2 GOES THIRD
    for k in c:                     # inner loop3 GOES FOURTH
      if i < 2 and j < 20 and k < 200:
        print((i, j, k))            # returned value GOES FIRST

对于迭代一个嵌套的列表/结构,技巧是一样的: 对于问题中的 a:

a = [[1,2],[3,4]]
[i2    for i1 in a      for i2 in i1]
which return [1, 2, 3, 4]

对于另一个嵌套级别

a = [[[1, 2], [3, 4]], [[5, 6], [7, 8, 9]], [[10]]]
[i3    for i1 in a      for i2 in i1     for i3 in i2]
which return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

等等,诸如此类


2
谢谢,但你所描述的实际上是涉及独立迭代器的简单情况。事实上,在你的例子中,你可以以_任何顺序_使用迭代器,并且会得到相同的结果列表(除了排序)。我更感兴趣的情况是嵌套列表的情况,其中一个迭代器成为下一个可迭代对象。 - ThomasH
@ThomasH:粗体定义的循环顺序正好符合您的需求。在底部添加了一个示例来涵盖您的数据,以及另一个带有额外嵌套级别的示例。 - Sławomir Lenart

25

ThomasH已经提供了一个好的回答,但我想展示发生了什么:

>>> a = [[1, 2], [3, 4]]
>>> [x for x in b for b in a]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined

>>> [x for b in a for x in b]
[1, 2, 3, 4]
>>> [x for x in b for b in a]
[3, 3, 4, 4]

我猜Python从左到右解析列表推导式。这意味着,首先出现的for循环将首先被执行。

第二个“问题”是b会从列表推导式中“泄漏”出来。在第一个成功的列表推导式之后,b == [3, 4]


4
有趣的一点。我对这个感到惊讶: x = 'hello'; [x for x in xrange(1,5)]; print x # x is now 4 - grinch
5
这个漏洞已经在 Python 3 中修复了:https://dev59.com/mW855IYBdhLWcg3wsWhq - Denilson Sá Maia
为什么它会引发“名称'b'未定义”的错误,即使它是一个局部变量?在这种情况下,为什么它不会首先引发“名称'x'未定义”的错误? - user0

14

在我的第一次尝试中,我从未能编写双列表推导式。阅读PEP202后,我发现原因是它的实现方式与英语阅读相反。好消息是,这是一个逻辑上正确的实现,因此一旦您理解了结构,就很容易正确地编写。

假设a、b、c、d都是嵌套对象。对我来说,扩展列表推导式的直观方法是模仿英语:

# works
[f(b) for b in a]
# does not work
[f(c) for c in b for b in a]
[f(c) for c in g(b) for b in a]
[f(d) for d in c for c in b for b in a]

换句话说,您将从底部开始阅读,即

# wrong logic
(((d for d in c) for c in b) for b in a)

然而,Python并不是按照这种方式实现嵌套列表。相反,该实现将第一个块视为完全分离的,然后从上到下(而不是从下到上)在一个单一的块中链接forin。即

# right logic
d: (for b in a, for c in b, for d in c)

请注意,最深层嵌套 (for d in c) 离列表中的最终对象 (d) 最远。这是由 Guido 本人 解释的:

表达式 [... for x... for y...] 是嵌套的,最后一个索引变化最快,就像嵌套的 for 循环一样。

使用 Skam 的文本示例,这一点变得更加清晰了:

# word: for sentence in text, for word in sentence
[word for sentence in text for word in sentence]

# letter: for sentence in text, for word in sentence, for letter in word
[letter for sentence in text for word in sentence for letter in word]

# letter:
#     for sentence in text if len(sentence) > 2, 
#     for word in sentence[0], 
#     for letter in word if letter.isvowel()
[letter for sentence in text if len(sentence) > 2 for word in sentence[0] for letter in word if letter.isvowel()]

1
小问题:你第一个代码段中的第一个示例确实可以工作([f(b) for b in a])。 - ThomasH
@ThomasH 已更新 :) - Martim

13

如果您想保留多维数组,则应嵌套数组括号。请参见以下示例,其中每个元素都加了1。

>>> a = [[1, 2], [3, 4]]

>>> [[col +1 for col in row] for row in a]
[[2, 3], [4, 5]]

>>> [col +1 for row in a for col in row]
[2, 3, 4, 5]

6

我觉得这样更容易理解

[row[i] for row in a for i in range(len(a))]

result: [1, 2, 3, 4]

3

此外,你可以使用当前访问的输入列表成员和该成员内部元素的同一变量。但是,这可能会使其更加(列表)难以理解。

input = [[1, 2], [3, 4]]
[x for x in input for x in x]

首先,for x in input 被计算出来,将输入的一个成员转换为列表,然后Python继续执行第二部分 for x in x,在此期间x值被当前正在访问的元素重写,然后第一个 x 定义了我们想要返回的内容。


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