Сопоставление списков Python для создания нескольких списков

Я хочу создать два списка listOfA и listOfB для хранения индексов A и B из другого списка.

s=['A','B','A','A','A','B','B']

Вывод должен состоять из двух списков

listOfA=[0,2,3,4]
listOfB=[1,5,6]

Я могу сделать это с двумя утверждениями.

listOfA=[idx for idx,x in enumerate(s) if x=='A']
listOfB=[idx for idx,x in enumerate(s) if x=='B']

Однако я хочу сделать это только в одной итерации, используя только списки. Можно ли сделать это в одном заявлении? что-то вроде listOfA,listOfB=[--code goes here--]

Ответ 1

Самым определением понимания списка является создание объекта один. Ваши 2 объекта списка имеют разную длину; вам нужно будет использовать побочные эффекты для достижения того, чего вы хотите.

Не используйте здесь списки. Просто используйте обычный цикл:

listOfA, listOfB = [], []

for idx, x in enumerate(s):
    target = listOfA if x == 'A' else listOfB
    target.append(idx)

Это приведет к тому, что вы выполните только один цикл; это побьет любые два понимания списков, по крайней мере, пока разработчики не найдут способ сделать переписку списков, строят список в два раза быстрее, чем цикл с отдельными вызовами list.append().

Я бы выбрал это в течение дня в соответствии с вложенным списком, чтобы иметь возможность создавать два списка на одной строке. Поскольку Zen of Python утверждает:

Показатели удобочитаемости.

Ответ 2

Сорт; ключ состоит в том, чтобы сгенерировать список из 2 элементов, который вы можете распаковать:

listOfA, listOfB = [[idx for idx, x in enumerate(s) if x == c] for c in 'AB']

Тем не менее, я думаю, что это довольно глупо делать это таким образом, явный цикл гораздо читабельнее.

Ответ 3

Хорошим подходом к этой проблеме является использование defaultdict. Как уже сказал @Martin, понимание списка не является правильным инструментом для создания двух списков. Использование defaultdict позволит вам создать сегрегацию с использованием одной итерации. Кроме того, ваш код не будет ограничен в любой форме.

>>> from collections import defaultdict
>>> s=['A','B','A','A','A','B','B']
>>> listOf = defaultdict(list)
>>> for idx, elem in enumerate(s):
    listOf[elem].append(idx)
>>> listOf['A'], listOf['B']
([0, 2, 3, 4], [1, 5, 6])

Ответ 4

То, что вы пытаетесь сделать, не совсем невозможно, это просто сложно и, вероятно, расточительно.

Если вы хотите разбить итерабельность на две итерации, если источник является списком или другим повторно используемым итерабельным, вам, вероятно, лучше сделать это за два прохода, как в вашем вопросе.

Даже если источником является итератор, если вы хотите получить пару списков, а не пару ленивых итераторов, либо используйте ответ Martijn, либо выполните два прохода над list(iterator).)

Но если вам действительно нужно лениво разделить произвольное итерабельное на два итерационных файла, нет никакого способа сделать это без какого-либо промежуточного хранилища.

Скажем, вы разделите [1, 2, -1, 3, 4, -2] на positives и negatives. Теперь вы пытаетесь сделать next(negatives). Это должно дать вам -1, верно? Но он не может этого сделать, не потребляя 1 и 2. Это означает, что когда вы пытаетесь сделать next(positives), вы получите 3 вместо 1. Итак, 1 и 2 нужно где-то хранить.

Большая часть необходимой вам itertools.tee внутри itertools.tee. Если вы просто делаете positives и negatives выражения в двух тидовых копиях одного и того же итератора, затем отфильтровывайте их оба, все готово.

Фактически, это один из рецептов в документах itertools:

def partition(pred, iterable):
    'Use a predicate to partition entries into false entries and true entries'
    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
    t1, t2 = tee(iterable)
    return filterfalse(pred, t1), filter(pred, t2)

(Если вы этого не понимаете, вероятно, стоит написать его явно, либо две функции генератора, разделяющие итератор и тройник через закрытие, либо два метода класса, которые разделяют их через self. Это должно быть несколько десятков строк кода, который не требует ничего сложного.)

И вы даже можете получить partition как импорт из сторонней библиотеки, такой как more_itertools.


Теперь вы можете использовать это в одном слое:

lst = [1, 2, -1, 3, 4, -2]
positives, negatives = partition(lst, lambda x: x>=0)

... и у вас есть итератор по всем положительным значениям, и итератор по всем отрицательным значениям. Они выглядят так, как будто они полностью независимы, но вместе они выполняют только один проход по lst поскольку он работает, даже если вы назначаете lst выражению генератора или файлу или чем-то вместо списка.


Итак, почему нет синтаксиса ярлыка для этого? Потому что это было бы довольно вводящим в заблуждение.

Понимание не требует дополнительного хранения. То, что генераторные выражения причины настолько велики - они могут превратить ленивый итератор в другой ленивый итератор, не сохраняя ничего.

Но это занимает память O(N). Представьте, что все цифры положительны, но сначала вы пытаетесь сначала перенести negative. Что просходит? Все числа попадают в trueq. Фактически, O(N) может даже быть бесконечным (например, попробуйте его на itertools.count()).

Это прекрасно для чего-то вроде itertools.tee, функции, застрявшей в модуле, о котором большинство новичков даже не знают, и у которого есть хорошие документы, которые могут объяснить, что он делает, и сделать издержки ясными. Но делать это с синтаксическим сахаром, который сделал его похожим на обычное понимание, было бы другой историей.

Ответ 5

Для тех, кто живет на краю;)

listOfA, listOfB = [[i for i in cur_list if i is not None] for cur_list in zip(*[(idx,None) if value == 'A' else (None,idx) for idx,value in enumerate(s)])]