Подвійна ітерація в розумінні списку


226

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

?? (Будь ласка, перерахуйте лише відповіді на розуміння, оскільки це я хочу дізнатися).

Відповіді:


178

Щоб відповісти на запитання власною пропозицією:

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

11
[x for b in a for x in b]Це завжди з помилками стосується пітона. Цей синтаксис настільки назад. Загальна форма x for x in yзавжди має змінну безпосередньо після for, подається на вираз зліва від for. Як тільки ви зробите подвійне розуміння, ваша остання ітераційна змінна раптом стає так "далеко". Це незручно і зовсім не читається природно
Cruncher

170

Я сподіваюся, що це допомагає комусь іншому, оскільки a,b,x,yне має для мене великого значення! Припустимо, у вас є текст, повний речень, і ви хочете масив слів.

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

8
"У комп'ютерних науках є лише дві важкі проблеми: недійсність кешу та іменування речей." - Філ Карлтон
цезар

Це чудова відповідь, оскільки робить всю проблему менш абстрактною! Дякую!
А. Блезій

Мені було цікаво, чи можете ви зробити те ж саме з трьома рівнями абстракції в розумінні списку? Як глави у тексті, речення у главах та слова у реченнях?
Капітан Фогетті

123

Дуже, я вважаю, що я знайшов прихильника: я недостатньо піклувався про те, яка петля є внутрішньою, а яка зовнішньою. Розуміння списку має бути таким:

[x for b in a for x in b]

щоб отримати бажаний результат, і так, одне поточне значення може бути ітератором для наступного циклу.


67
Синтаксис розуміння списку - не одна з яскравих точок Python.
Гленн Мейнард

2
@Glenn Так, це легко заплутується для більш ніж простих виразів.
ThomasH

1
Ew Я не впевнений, що це "звичайне" використання для розуміння списків, але дуже прикро, що ланцюжок настільки неприємний у Python.
Метт Столяр

14
Це виглядає дуже чисто, якщо ви ставите нові рядки перед кожним «за».
Нік Гарві

16
Нічого собі, це абсолютно зворотно до того, що має сенс у моїй голові.
obskyr

51

Порядок ітераторів може здатися протиінтуїтивним.

Візьмемо для прикладу: [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)

4
Що за відкривання очей !!
nehem

Я розумію, що причина цього полягає в тому, що "перша перерахована ітерація - це найвища ітерація, яка була б набрана, якби розуміння було написано як вкладене для циклів". Причиною цього є протиінтуїтивність у тому, що цикл OUTER (верхній, якщо він написаний як вкладений для-циклів) з'являється у ВНУТРІ закресленого списку / dict (об'єкта розуміння). І навпаки, цикл INNER (найпотаємніший, коли він пишеться як вкладений для-циклів) - це саме правий цикл у розумінні, і таким чином з'являється на ВІННЯМИ розуміння.
Zach Siegel

Абстрактно написане у нас [(output in loop 2) (loop 1) (loop 2)]з (loop 1) = for i in range(3)і (loop 2) = for x in foo(i):і (output in loop 2) = str(x).
Qaswed

20

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


3
Цікавий момент. Я здивувався цьому:x = 'hello'; [x for x in xrange(1,5)]; print x # x is now 4
усміхнувся

2
Цей витік був зафіксований у Python 3: stackoverflow.com/questions/4198906/…
Denilson Sá Maia

10

Якщо ви хочете зберегти багатовимірний масив, слід вкласти дужки масивів. див. приклад нижче, де кожен додається до кожного елемента.

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

Ця техніка пам'яті мені дуже допомагає:

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

А тепер ви можете подумати про R eturn + O маткову петлю як єдиний R іght O rder

Знаючи вище, порядок у списку є вичерпним навіть для 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]

і так далі


Дякую, але те, що ви описуєте, насправді є простим випадком, коли залучені ітератори незалежні. Насправді, у вашому прикладі ви могли використовувати ітератори в будь-якому порядку і отримали б той самий список результатів (впорядкування по модулю). Мене більше цікавили випадки з вкладеними списками, де один ітератор стає наступним.
ThomasH

@ThomasH: порядок циклу, визначений жирним шрифтом, відповідає саме вашим потребам. Знизу додано приклад для покриття ваших даних та ще один приклад із додатковим рівнем вкладеності.
Славомір Ленарт

5

Я відчуваю, що це легше зрозуміти

[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визначає, що ми хочемо повернути.


1

Ця функція flatten_nlevel викликає рекурсивно вкладений список1 для переходу на один рівень. Спробуйте це

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)

вихід:

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

1
Гаразд, питання стосувалося особливо розуміння списків, а вирівнювання списків - лише приклад. Але я припускаю, що вашому узагальненому лістивнику списку потрібно буде називати себе рекурсивно. Так це, мабуть, більше схоже flatten_nlevel(sublist, flat_list), правда ?!
ThomasH
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.