Doble iteración en la comprensión de listas

296

En Python puede tener varios iteradores en una lista de comprensión, como

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

para algunas secuencias adecuadas ay b. Soy consciente de la semántica de bucle anidado de las listas por comprensión de Python.

Mi pregunta es: ¿Puede un iterador en la comprensión referirse al otro? En otras palabras: ¿Podría tener algo como esto?

[x for x in a for a in b]

donde el valor actual del bucle exterior es el iterador del interior?

Como ejemplo, si tengo una lista anidada:

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

cuál sería la expresión de comprensión de la lista para lograr este resultado:

[1,2,3,4]

?? (Por favor, enumere solo las respuestas de comprensión, ya que esto es lo que quiero averiguar).

0
281

¡Espero que esto ayude a alguien más, ya a,b,x,yque no tiene mucho significado para mí! Suponga que tiene un texto lleno de oraciones y desea una variedad de palabras.

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

Me gusta pensar en la comprensión de listas como una extensión del código horizontalmente.

Intente dividirlo en:

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

Ejemplo:

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

Esto también funciona para generadores.

>>> 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?
7
  • 23
    "Sólo hay dos problemas difíciles en Ciencias de la Computación: invalidación de caché y nombrar cosas". - Phil Karlton
    cezar
    8 de agosto de 2018 a las 8:42
  • 1
    ¡Esta es una gran respuesta ya que hace que todo el problema sea menos abstracto! ¡Gracias! 16/03/20 a las 13:13
  • 1
    Alternativamente, puede: [[palabra por palabra en la oración] para la oración en el texto]
    Saskia
    29 jul.20 a las 10:02
  • 2
    @Saskia No del todo. Esto solo le devolverá la misma entrada. ¿Ves por qué?
    Skam
    29/07/20 a las 16:59
  • 1
    Solo estaba sugiriendo una sintaxis para recorrer las dos comprensiones sin esta lógica hacia atrás. No relacionado con la resolución de la pregunta original.
    Saskia
    29 jul.20 a las 17:36
189

Para responder a su pregunta con su propia sugerencia:

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

Si bien solicitó respuestas de comprensión de la lista, permítame también señalar el excelente itertools.chain ():

>>> from itertools import chain
>>> list(chain.from_iterable(a))
>>> list(chain(*a)) # If you're using python < 2.6
1
  • 32
    [x for b in a for x in b]Esto siempre ha tenido que ver con Python. Esta sintaxis es tan al revés. La forma general de x for x in ysiempre tiene la variable directamente después de la for, alimenta la expresión a la izquierda de la for. Tan pronto como hagas una doble comprensión, tu variable iterada más recientemente es repentinamente tan "lejos". Es incómodo y no se lee de forma natural en absoluto. 18/02/20 a las 19:41
143

Vaya, creo que encontré la respuesta: no me estaba preocupando lo suficiente por qué bucle es interno y cuál es externo. La comprensión de la lista debería ser como:

[x for b in a for x in b]

para obtener el resultado deseado, y sí, un valor actual puede ser el iterador para el siguiente ciclo.

6
  • 82
    La sintaxis de comprensión de listas no es uno de los puntos brillantes de Python. 29 de julio de 2009 a las 8:37
  • 4
    @Glenn Sí, se complica fácilmente para algo más que simples expresiones.
    ThomasH
    29 de julio de 2009 a las 8:53
  • 1
    Ew. No estoy seguro de que este sea el uso "habitual" para las listas por comprensión, pero es muy desafortunado que el encadenamiento sea tan desagradable en Python. 28/08/11 a las 10:24
  • 19
    Se ve muy limpio si pones nuevas líneas antes de cada 'para'. 11 de septiembre de 2014 a las 7:03
  • 22
    Vaya, esto es completamente inverso a lo que tiene sentido en mi cabeza.
    obskyr
    21 de junio de 2017 a las 10:03
58

El orden de los iteradores puede parecer contrario a la intuición.

Toma por ejemplo: [str(x) for i in range(3) for x in foo(i)]

Vamos a descomponerlo:

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)
3
  • 5
    ¡Qué revelador!
    nehem
    28 de junio de 2017 a las 3:44
  • Tengo entendido que la razón de esto es que "la primera iteración enumerada es la iteración más alta que se escribiría si la comprensión se escribiera como bucles for anidados". La razón por la que esto es contrario a la intuición es que el bucle OUTER (el más alto si está escrito como bucles for anidados) aparece en el INTERIOR de la lista / dict entre corchetes (objeto comprendido). A la inversa, el bucle INNER (el más interno cuando se escribe como bucles for anidados) es precisamente el bucle más a la derecha en una comprensión, y de esa manera aparece en el EXTERIOR de la comprensión. 6 de agosto de 2017 a las 6:31
  • 1
    De forma abstracta tenemos [(output in loop 2) (loop 1) (loop 2)]con (loop 1) = for i in range(3)y (loop 2) = for x in foo(i):y (output in loop 2) = str(x).
    Qaswed
    10/07/19 a las 9:58
25

ThomasH ya ha agregado una buena respuesta, pero quiero mostrar lo que sucede:

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

Supongo que Python analiza la comprensión de la lista de izquierda a derecha. Esto significa que el primer forbucle que se produzca se ejecutará primero.

El segundo "problema" de esto es que bse "filtra" fuera de la lista de comprensión. Después de la primera comprensión exitosa de la lista b == [3, 4].

2
20

Esta técnica de memoria me ayuda mucho:

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

Y ahora se puede pensar en R eturn + O Uter-bucle como el único R vuelo O rden

Sabiendo lo anterior, el orden en la lista completa incluso para 3 bucles parece fácil:


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

porque lo anterior es solo 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

para iterar una lista / estructura anidada, la técnica es la misma: ade la pregunta:

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

el uno para el otro nivel anidado

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étera

2
  • 2
    Gracias, pero lo que describe es en realidad el caso simple en el que los iteradores involucrados son independientes. De hecho, en su ejemplo, podría usar los iteradores en cualquier orden y obtendría la misma lista de resultados (orden de módulo). El caso en el que estaba más interesado fue con listas anidadas donde un iterador se convierte en iterable del siguiente.
    ThomasH
    5 de ene. De 2020 a las 15:15
  • @ThomasH: el orden del ciclo definido en negrita es exactamente para su necesidad. En la parte inferior, se agregó un ejemplo para cubrir sus datos y un ejemplo más con un nivel adicional anidado. 5 de ene. De 2020 a las 18:01
13

Si desea mantener la matriz multidimensional, debe anidar los corchetes de la matriz. vea el ejemplo a continuación donde se agrega uno a cada elemento.

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

Nunca pude escribir la comprensión de doble lista en mi primer intento. Al leer PEP202 , resulta que la razón es que se implementó de la manera opuesta a la que se leería en inglés. La buena noticia es que es una implementación lógicamente sólida, por lo que una vez que comprenda la estructura, es muy fácil hacerlo bien.

Sean a, b, c, d objetos sucesivamente anidados. Para mí, la forma intuitiva de ampliar la comprensión de la lista imitaría el inglés:

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

En otras palabras, estarías leyendo de abajo hacia arriba, es decir

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

Sin embargo, no es así como Python implementa listas anidadas. En cambio, la implementación trata el primer fragmento como completamente separado y luego encadena las fors y las ins en un solo bloque de arriba hacia abajo (en lugar de de abajo hacia arriba), es decir

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

Tenga en cuenta que el nivel anidado más profundo ( for d in c) es el más alejado del objeto final en la lista ( d). La razón de esto proviene del propio Guido :

The form [... for x... for y...] nests, with the last index varying fastest, just like nested for loops.

Usando el ejemplo de texto de Skam, esto se vuelve aún más claro:

# 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()]
2
  • 1
    Minor nit: el primer ejemplo en su primera sección de código sí funciona ( [f(b) for b in a]).
    ThomasH
    20 abr a las 10:08
  • @ThomasH Actualizado :)
    Martim
    20 abr a las 22:22
5

Siento que esto es más fácil de entender

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

result: [1, 2, 3, 4]
3

Además, puede usar la misma variable para el miembro de la lista de entrada a la que se accede actualmente y para el elemento dentro de este miembro. Sin embargo, esto incluso podría hacerla más (lista) incomprensible.

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

Primero for x in inputse evalúa, lo que lleva a una lista de miembros de la entrada, luego, Python recorre la segunda parte for x in xdurante la cual el valor x es sobrescrito por el elemento actual al que está accediendo, luego la primera xdefine lo que queremos devolver.

1

Esta función flatten_nlevel llama de forma recursiva a list1 anidado para convertirlo en un nivel. Probar esto

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)

producción:

[1, 1, 2, 3, 4, 6, 4, 5]
1
  • 1
    Ok, la pregunta se refería particularmente a la comprensión de listas, y el aplanamiento de listas fue solo un ejemplo. Pero supongo que su acoplador de listas generalizado necesitaría llamarse a sí mismo de forma recursiva. Así que probablemente sea más como flatten_nlevel(sublist, flat_list), ¿verdad?
    ThomasH
    7 de mayo de 2020 a las 7:57