Double itération dans la compréhension de liste

226

En Python, vous pouvez avoir plusieurs itérateurs dans une liste de compréhension, comme

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

pour certaines séquences appropriées a et b. Je connais la sémantique des boucles imbriquées des compréhensions de liste de Python.

Ma question est: un itérateur dans la compréhension peut-il se référer à l'autre? En d'autres termes: pourrais-je avoir quelque chose comme ceci:

[x for x in a for a in b]

où la valeur actuelle de la boucle extérieure est l'itérateur de l'intérieur?

Par exemple, si j'ai une liste imbriquée:

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

quelle serait l'expression de compréhension de liste pour atteindre ce résultat:

[1,2,3,4]

?? (Veuillez énumérer uniquement les réponses de compréhension, car c'est ce que je veux savoir).

ThomasH
la source

Réponses:

178

Pour répondre à votre question avec votre propre suggestion:

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

Alors que vous avez demandé des réponses de compréhension de liste, permettez-moi également de souligner l'excellent itertools.chain ():

>>> from itertools import chain
>>> list(chain.from_iterable(a))
>>> list(chain(*a)) # If you're using python < 2.6
Cide
la source
11
[x for b in a for x in b]Cela a toujours été un bug sur python. Cette syntaxe est tellement à l'envers. La forme générale de a x for x in ytoujours la variable directement après le for, alimente l'expression à gauche du for. Dès que vous effectuez une double compréhension, votre variable itérée la plus récente est soudainement "si loin". C'est maladroit, et ne lit pas du tout naturellement
Cruncher
170

J'espère que cela aide quelqu'un d'autre car a,b,x,ycela n'a pas beaucoup de sens pour moi! Supposons que vous ayez un texte plein de phrases et que vous vouliez un tableau de mots.

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

J'aime penser à la compréhension de liste comme étirant du code horizontalement.

Essayez de le décomposer en:

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

Exemple:

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

Cela fonctionne également pour les générateurs

>>> 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?
Skam
la source
8
"Il n'y a que deux problèmes difficiles en informatique: l'invalidation du cache et le nommage." - Phil Karlton
cezar
C'est une excellente réponse car elle rend l'ensemble du problème moins abstrait! Je vous remercie!
A. Blesius
Je me demandais, pouvez-vous faire de même avec trois niveaux d'abstraction dans une liste de compréhension? Comme les chapitres dans le texte, les phrases dans les chapitres et les mots dans les phrases?
Capitaine Fogetti
123

Gee, je suppose que j'ai trouvé la réponse: je ne faisais pas assez attention à quelle boucle est intérieure et laquelle est extérieure. La compréhension de la liste doit être comme:

[x for b in a for x in b]

pour obtenir le résultat souhaité, et oui, une valeur actuelle peut être l'itérateur de la boucle suivante.

ThomasH
la source
67
La syntaxe de compréhension de liste n'est pas l'un des points forts de Python.
Glenn Maynard
2
@Glenn Oui, il est facilement compliqué pour plus que de simples expressions.
ThomasH
1
Ew. Je ne suis pas sûr que ce soit l'utilisation "habituelle" pour les compréhensions de liste, mais il est très regrettable que le chaînage soit si méchant en Python.
Matt Joiner
14
Il semble très propre si vous mettez des sauts de ligne avant chaque "pour".
Nick Garvey
16
Wow, c'est complètement inverse de ce qui a du sens dans ma tête.
obskyr
51

L'ordre des itérateurs peut sembler contre-intuitif.

Prends pour exemple: [str(x) for i in range(3) for x in foo(i)]

Décomposons-le:

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)
Dima Tisnek
la source
4
Quelle révélation !!
nehem
Ma compréhension est que la raison en est que "la première itération répertoriée est l'itération la plus élevée qui serait tapée si la compréhension était écrite comme imbriquée pour les boucles". La raison pour laquelle cela est contre-intuitif est que la boucle OUTER (la plus haute si elle est écrite en tant que boucles imbriquées) apparaît à L'INTÉRIEUR de la liste / dict entre crochets (objet de compréhension). Inversement, la boucle INNER (la plus interne lorsqu'elle est écrite en tant que boucles imbriquées) est précisément la boucle la plus à droite dans une compréhension, et apparaît de cette façon à L'EXTÉRIEUR de la compréhension.
Zach Siegel
Résumé nous avons écrit [(output in loop 2) (loop 1) (loop 2)]avec (loop 1) = for i in range(3)et (loop 2) = for x in foo(i):et (output in loop 2) = str(x).
Qaswed
20

ThomasH a déjà ajouté une bonne réponse, mais je veux montrer ce qui se passe:

>>> 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]

Je suppose que Python analyse la compréhension de la liste de gauche à droite. Cela signifie que la première forboucle qui se produit sera exécutée en premier.

Le deuxième "problème" de ceci est que b"la fuite" de la compréhension de la liste. Après la première compréhension réussie de la liste b == [3, 4].

Martin Thoma
la source
3
Point intéressant. J'étais surpris de cela:x = 'hello'; [x for x in xrange(1,5)]; print x # x is now 4
grinch
2
Cette fuite a été corrigée dans Python 3: stackoverflow.com/questions/4198906/…
Denilson Sá Maia
10

Si vous souhaitez conserver le tableau multidimensionnel, il faut imbriquer les supports du tableau. voir l'exemple ci-dessous où un est ajouté à chaque élément.

>>> 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]
Steven
la source
8

Cette technique de mémoire m'aide beaucoup:

[ <RETURNED_VALUE> <OUTER_LOOP1> <INNER_LOOP2> <INNER_LOOP3> ... <OPTIONAL_IF> ]

Et maintenant, vous pouvez penser à R eturn + O uter-loop comme le seul R ight O rder

Sachant ci-dessus, l'ordre dans la liste complet même pour 3 boucles semble simple:


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)]

car ce qui précède n'est qu'un:

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

pour itérer une liste / structure imbriquée, la technique est la même: pour ala question:

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

pour un autre niveau imbriqué

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]

etc

Sławomir Lenart
la source
Merci, mais ce que vous décrivez est en fait le cas simple où les itérateurs impliqués sont indépendants. En fait, dans votre exemple, vous pouvez utiliser les itérateurs dans n'importe quel ordre et obtenir la même liste de résultats (commande modulo). Le cas qui m'intéressait le plus était celui des listes imbriquées où un itérateur devient l'itérable du suivant.
ThomasH
@ThomasH: l'ordre de boucle défini en gras est exactement pour votre besoin. En bas, ajouté un exemple pour couvrir vos données et un autre exemple avec niveau imbriqué supplémentaire.
Sławomir Lenart
5

Je pense que c'est plus facile à comprendre

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

result: [1, 2, 3, 4]
Miao Li
la source
3

De plus, vous pouvez utiliser exactement la même variable pour le membre de la liste d'entrée actuellement accessible et pour l'élément à l'intérieur de ce membre. Cependant, cela pourrait même le rendre plus (liste) incompréhensible.

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

La première for x in inputest évaluée, conduisant à une liste de membres de l'entrée, puis, Python parcourt la deuxième partie for x in xpendant laquelle la valeur x est écrasée par l'élément actuel auquel il accède, puis la première xdéfinit ce que nous voulons retourner.

simP
la source
1

Cette fonction flatten_nlevel appelle récursivement la liste imbriquée1 pour la convertir à un niveau. Essayez ceci

def flatten_nlevel(list1, flat_list):
    for sublist in list1:
        if isinstance(sublist, type(list)):        
            flatten_nlevel(sublist, flat_list)
        else:
            flat_list.append(sublist)

list1 = [1,[1,[2,3,[4,6]],4],5]

items = []
flatten_nlevel(list1,items)
print(items)

production:

[1, 1, 2, 3, 4, 6, 4, 5]
ravibeli
la source
1
Ok, la question portait particulièrement sur la compréhension des listes, et l'aplatissement des listes n'était qu'un exemple. Mais je suppose que votre aplatisseur de liste généralisée devrait s'appeler de manière récursive. Donc c'est probablement plus comme ça flatten_nlevel(sublist, flat_list), non?!
ThomasH