Pythonic способ объединить два списка поочередно?

88

У меня есть два списка, первый из которых гарантированно будет содержать ровно на один элемент больше, чем второй . Я хотел бы знать, какой способ создания нового списка с помощью Python - четный, чьи значения четного индекса берутся из первого списка, а значения нечетного индекса - из второго списка.

# example inputs
list1 = ['f', 'o', 'o']
list2 = ['hello', 'world']

# desired output
['f', 'hello', 'o', 'world', 'o']

Это работает, но не очень красиво:

list3 = []
while True:
    try:
        list3.append(list1.pop(0))
        list3.append(list2.pop(0))
    except IndexError:
        break

Как еще этого можно достичь? Какой самый питонический подход?

Дэвидчемберс
источник
1
возможный дубликат чередования итераторов в Python
Феликс Клинг,
Не дубликат! Принятый ответ в указанной выше статье создает список кортежей, а не единый объединенный список.
Пол Сасик,
@ Пол: Да, принятый ответ не дает полного решения. Прочтите комментарии и другие ответы. Вопрос в основном тот же, и здесь можно применить другие решения.
Феликс Клинг,
3
@ Феликс: Я с уважением не согласен. Это правда, вопросы находятся в одном районе, но не дублируются. В качестве нечеткого доказательства взгляните на возможные ответы здесь и сравните с другим вопросом.
Пол Сасик,
Обратите внимание на это: stackoverflow.com/questions/7529376/…
wordsforthewise

Ответы:

112

Вот один из способов сделать это, нарезав:

>>> list1 = ['f', 'o', 'o']
>>> list2 = ['hello', 'world']
>>> result = [None]*(len(list1)+len(list2))
>>> result[::2] = list1
>>> result[1::2] = list2
>>> result
['f', 'hello', 'o', 'world', 'o']
Дункан
источник
3
Спасибо, Дункан. Я не понимал, что можно указать шаг при нарезке. Что мне нравится в этом подходе, так это то, насколько естественно он читается. 1. Составьте список правильной длины. 2. Заполняет четные индексы содержимым list1. 3. Заполните нечетные индексы содержимым списка list2. Тот факт, что списки имеют разную длину, в данном случае не проблема!
davidchambers
2
Я думаю, это работает только тогда, когда len (list1) - len (list2) равно 0 или 1.
xan
1
Если списки имеют подходящую длину, тогда это работает, если нет, то исходный вопрос не указывает, какой ответ ожидается. Его можно легко изменить для обработки наиболее разумных ситуаций: например, если вы хотите, чтобы лишние элементы игнорировались, просто сократите более длинный список перед тем, как начать; если вы хотите, чтобы дополнительные элементы чередовались с None, просто убедитесь, что результат инициализирован еще несколькими None; если вы хотите, чтобы дополнительные элементы просто добавлялись в конце, сделайте то же, что и для их игнорирования, а затем добавьте их.
Дункан,
1
Мне тоже было непонятно. Я пытался подчеркнуть, что решение Дункана, в отличие от многих из перечисленных, не усложняется тем, что списки имеют неравную длину. Конечно, это применимо только в ограниченном диапазоне ситуаций, но я бы предпочел действительно элегантное решение, которое работает в этом случае, менее элегантному решению, которое работает для любых двух списков.
davidchambers
1
Вы можете использовать (2 * len (list1) -1) вместо (len (list1) + len (list2)), также я предпочитаю [0 :: 2] вместо [:: 2].
Lord British
50

Там рецепт для этого в itertoolsдокументации :

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).next for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))

РЕДАКТИРОВАТЬ:

Для версии Python выше 3:

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))
Дэвид З
источник
Я считаю этот способ более сложным, чем нужно. Ниже есть лучший вариант с использованием zip_longest.
Dubslow
@Dubslow. Для этого конкретного случая, да, это, вероятно, излишек (как я уже упоминал в другом месте), если только у вас уже нет к нему доступа. Однако это может иметь некоторые преимущества в других ситуациях. Этот рецепт определенно не предназначен для этой проблемы, он просто решает ее.
David Z
1
fyi, вы должны использовать рецепт из itertools документации, потому что он .next()больше не работает.
Джон У.
1
@johnw. нужно использовать __next__. Этого нет в документации, поэтому я предложил отредактировать ответ.
Марин Галантин,
@Marine Я бы предпочел, чтобы вы только что изменили существующий образец кода, но я могу исправить это сам. Спасибо за участие!
Дэвид Зи,
31

Это должно делать то, что вы хотите:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']
Марк Байерс
источник
Мне очень понравился твой первоначальный ответ. Хотя это не совсем ответ на вопрос, это был элегантный способ объединить два списка одинаковой длины. Я предлагаю сохранить его вместе с оговоркой о длине в вашем текущем ответе.
Пол Сасик,
1
Если бы list1 был вместо этого ['f', 'o', 'o', 'd'], его последний элемент ('d') не появился бы в итоговом списке (что совершенно нормально, учитывая специфику вопроса). Это шикарное решение!
davidchambers
1
@Mark да (я проголосовал за это), просто указывая на различия (и ограничения, если другие люди хотят другого поведения)
cobbal
4
+1 за решение заявленной проблемы, да и за то, чтобы сделать это просто :-) Я подумал, что что-то подобное возможно. Честно говоря, я считаю, что в roundrobinэтой ситуации эта функция излишняя.
David Z
1
Для работы со списками любого размера вы можете просто добавить к результату то, что осталось в итераторах:list(itertools.chain(map(next, itertools.cycle(iters)), *iters))
panda-34
29
import itertools
print [x for x in itertools.chain.from_iterable(itertools.izip_longest(list1,list2)) if x]

Я думаю, что это самый питонический способ сделать это.

user942640
источник
3
Почему это не принятый ответ? Это самый короткий и самый питонический вариант, работающий со списками разной длины!
Хайро Вадилло
5
имя метода - zip_longest, а не izip_longest
Хайро
1
Проблема в том, что значение заполнения по умолчанию из zip_longest может перезаписать Nones, которые * должны быть в списке. Я отредактирую исправленную версию, чтобы исправить это
Дабслоу
Примечание: Это может вызвать проблемы , если эти списки содержат элементы со значением False, или даже вещи , которые будут только оценены , как Falseв if-expression, как, например, 0или пустой список. Это может быть (частично) избежать следующего: [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]. Конечно, это все равно не сработает, если в списках есть Noneэлементы, которые необходимо сохранить. В этом случае вам нужно изменить fillvalueаргумент zip_longest, как уже предлагал Дабслоу.
der_herr_g
Noneпроблема, похоже, исчезла, по крайней мере, с Python 3.7.6 (я не знаю для более старых версий). Если alt_chainопределено как def alt_chain(*iters, fillvalue=None): return chain.from_iterable(zip_longest(*iters, fillvalue=fillvalue)), то list(alt_chain([0, False, 1, set(), 3, 4], [0, None, 1, {}], fillvalue=99))вернет правильно [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99].
paime
17

Без itertools и при условии, что l1 на 1 элемент длиннее l2:

>>> sum(zip(l1, l2+[0]), ())[:-1]
('f', 'hello', 'o', 'world', 'o')

Используя itertools и предполагая, что списки не содержат None:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')
Zart
источник
Это мой любимый ответ. Это так лаконично.
mbomb007
@ anishtain4 зип принимает пар элементов , как кортежи из списков [(l1[0], l2[0]), (l1[1], l2[1]), ...]. sumобъединяет кортежи вместе: в (l1[0], l2[0]) + (l1[1], l2[1]) + ...результате получаются чередующиеся списки. Остальная часть однострочной строки - это просто заполнение l1 дополнительным элементом для работы zip и нарезкой до -1, чтобы избавиться от этого заполнения.
Зарт
izip_longest (zip_longest с Python 3) не требует заполнения + [0], он неявно заполняет None, когда длины списков не совпадают, в то время как filter(None, ...(может использовать boolвместо этого или None.__ne__) удаляет ложные значения, включая 0, None и пустые строки, поэтому второе выражение не является строго эквивалентным первому.
Зарт
Вопрос в том, как вы sumэто сделали? Какую роль здесь играет второй аргумент? В документации второй аргумент start.
anishtain4
Значение start по умолчанию равно 0, и вы не можете сделать 0+ (some, tuple), поэтому start заменяется на пустой кортеж.
Зарт
13

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

Вот решение Дункана, адаптированное для работы с двумя списками разного размера.

list1 = ['f', 'o', 'o', 'b', 'a', 'r']
list2 = ['hello', 'world']
num = min(len(list1), len(list2))
result = [None]*(num*2)
result[::2] = list1[:num]
result[1::2] = list2[:num]
result.extend(list1[num:])
result.extend(list2[num:])
result

Это выводит:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 
mhost
источник
6

Если оба списка имеют одинаковую длину, вы можете:

[x for y in zip(list1, list2) for x in y]

Поскольку в первом списке есть еще один элемент, вы можете добавить его постфактум:

[x for y in zip(list1, list2) for x in y] + [list1[-1]]
Кто то
источник
2
^ Это должно быть ответом, питон стал более питоническим за последние 10 лет
Тиан
5

Вот один лайнер, который это делает:

list3 = [ item for pair in zip(list1, list2 + [0]) for item in pair][:-1]

Джей
источник
2
Это работает правильно, но мне кажется неэлегантным, поскольку он так много делает для достижения чего-то столь простого. Я не говорю, что этот подход неэффективен, просто его нелегко читать.
davidchambers
2
def combine(list1, list2):
    lst = []
    len1 = len(list1)
    len2 = len(list2)

    for index in range( max(len1, len2) ):
        if index+1 <= len1:
            lst += [list1[index]]

        if index+1 <= len2:
            lst += [list2[index]]

    return lst
печь
источник
Будьте очень осторожны при использовании изменяемых аргументов по умолчанию. Это вернет правильный ответ только при первом вызове, так как lst будет повторно использоваться для каждого последующего вызова. Это лучше было бы записать как lst = None ... если lst равно None: lst = [], хотя я не вижу веских причин выбирать этот подход по сравнению с другими, перечисленными здесь.
davidchambers
lst определяется внутри функции, поэтому является локальной переменной. Потенциальная проблема заключается в том, что list1 и list2 будут повторно использоваться каждый раз, когда вы используете функцию, даже если вы вызываете функцию с разными списками. См. Docs.python.org/tutorial/…
blokeley
1
@blokeley: неправильно, было бы повторно использовано, если бы он был объединен (list1 = [...], list2 = [...])
killown
Когда это решение было впервые опубликовано, его первая строка прочитана def combine(list1, list2, lst=[]):, отсюда и мой комментарий. Однако к тому времени, когда я отправил этот комментарий, killown внесла необходимые изменения.
davidchambers
2

Это основано на приведенном выше вкладе Карлоса Валиенте с возможностью чередовать группы из нескольких элементов и убедиться, что все элементы присутствуют на выходе:

A=["a","b","c","d"]
B=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]

def cyclemix(xs, ys, n=1):
    for p in range(0,int((len(ys)+len(xs))/n)):
        for g in range(0,min(len(ys),n)):
            yield ys[0]
            ys.append(ys.pop(0))
        for g in range(0,min(len(xs),n)):
            yield xs[0]
            xs.append(xs.pop(0))

print [x for x in cyclemix(A, B, 3)]

Это будет чередовать списки A и B группами по 3 значения в каждой:

['a', 'b', 'c', 1, 2, 3, 'd', 'a', 'b', 4, 5, 6, 'c', 'd', 'a', 7, 8, 9, 'b', 'c', 'd', 10, 11, 12, 'a', 'b', 'c', 13, 14, 15]
catpnced
источник
2

Возможно, будет немного поздно купить еще один однострочник на Python. Это работает, когда два списка имеют равный или неравный размер. Одна вещь ничего не стоит - это изменит a и b. Если это проблема, вам нужно использовать другие решения.

a = ['f', 'o', 'o']
b = ['hello', 'world']
sum([[a.pop(0), b.pop(0)] for i in range(min(len(a), len(b)))],[])+a+b
['f', 'hello', 'o', 'world', 'o']
Аллен
источник
1

Мое мнение:

a = "hlowrd"
b = "el ol"

def func(xs, ys):
    ys = iter(ys)
    for x in xs:
        yield x
        yield ys.next()

print [x for x in func(a, b)]
Карлос Валиенте
источник
1

Вот один лайнер с использованием списков без других библиотек:

list3 = [sub[i] for i in range(len(list2)) for sub in [list1, list2]] + [list1[-1]]

Вот еще один подход, если вы разрешаете изменение исходного списка1 по побочным эффектам:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]
Черневик
источник
1
from itertools import chain
list(chain(*zip('abc', 'def')))  # Note: this only works for lists of equal length
['a', 'd', 'b', 'e', 'c', 'f']
Кен Сихарт
источник
0

Остановки по кратчайшему:

def interlace(*iters, next = next) -> collections.Iterable:
    """
    interlace(i1, i2, ..., in) -> (
        i1-0, i2-0, ..., in-0,
        i1-1, i2-1, ..., in-1,
        .
        .
        .
        i1-n, i2-n, ..., in-n,
    )
    """
    return map(next, cycle([iter(x) for x in iters]))

Конечно, разрешение метода next / __ next__ может быть быстрее.

jwp
источник
0

Это неприятно, но работает независимо от размера списков:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]
рыцарь-паук
источник
0

Несколько однострочников, вдохновленных ответами на другой вопрос :

import itertools

list(itertools.chain.from_iterable(itertools.izip_longest(list1, list2, fillvalue=object)))[:-1]

[i for l in itertools.izip_longest(list1, list2, fillvalue=object) for i in l if i is not object]

[item for sublist in map(None, list1, list2) for item in sublist][:-1]
слова
источник
0

Как насчет numpy? Он также работает со строками:

import numpy as np

np.array([[a,b] for a,b in zip([1,2,3],[2,3,4,5,6])]).ravel()

Результат:

array([1, 2, 2, 3, 3, 4])
Николай Фрик
источник
0

Альтернатива функциональным и неизменным способом (Python 3):

from itertools import zip_longest
from functools import reduce

reduce(lambda lst, zipped: [*lst, *zipped] if zipped[1] != None else [*lst, zipped[0]], zip_longest(list1, list2),[])
Годо
источник
-1

Я бы сделал простое:

chain.from_iterable( izip( list1, list2 ) )

Он предложит итератор, не создавая дополнительных потребностей в хранилище.

пшеница
источник
1
Это действительно просто, но работает только со списками одинаковой длины!
Йохен Ритцель,
Вы можете исправить это с chain.from_iterable(izip(list1, list2), list1[len(list2):])помощью конкретной проблемы, заданной здесь ... list1 должен быть более длинным.
Йохен Ритцель,
Да, но я бы предпочел либо решение, которое работает для контейнеров произвольной длины, либо уступить решениям, предложенным выше.
Wheaties
-2

Я слишком стар, чтобы отказываться от понимания списков, поэтому:

import operator
list3 = reduce(operator.add, zip(list1, list2))
Том Андерсон
источник