Пітонічний спосіб поєднання двох списків по черзі?


88

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

# 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

Як ще цього можна досягти? Який найбільш пітонічний підхід?



Не дублікат! Прийнята відповідь у вищезгаданій статті дає список кортежів, а не єдиний, об’єднаний список.
Paul Sasik,

@ Пол: Так, прийнята відповідь не дає повного рішення. Прочитайте коментарі та інші відповіді. Питання в основному однакове, і тут можна застосувати інші рішення.
Фелікс Клінг,

3
@Felix: Я з повагою не погоджуюсь. Це правда, питання знаходяться в одному районі, але насправді не повторюються. Як розмитий доказ подивіться на потенційні відповіді тут і порівняйте з іншим питанням.
Paul Sasik,

Відповіді:


110

Ось один із способів зробити це шляхом нарізки:

>>> 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 У цьому конкретному випадку, так, це, мабуть, надмірно (як я вже згадував у коментарі в іншому місці), якщо ви вже не мали до нього доступу. Однак це може мати певні переваги в інших ситуаціях. Цей рецепт, звичайно, не був розроблений для цієї проблеми, він просто вирішує її.
David Z

1
fyi, ви повинні використовувати рецепт у itertools документації, оскільки він .next()більше не працює.
Джон ш.

1
@johnw. треба використовувати __next__. Це не написано в документації, тому я запропонував змінити відповідь.
Marine Galantin

@Marine Я б скоріше сказав, що ви щойно змінили існуючий зразок коду, але я можу це виправити сам. Дякуємо за внесок!
David Z

31

Це має робити те, що ви хочете:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']

Мені дуже сподобалась ваша початкова відповідь. Хоча це питання не відповідало ідеально, це був елегантний спосіб об’єднати два списки однакової довжини. Я пропоную зберегти його разом із застереженням щодо довжини у вашій поточній відповіді.
Paul Sasik,

1
Якби натомість list1 був ['f', 'o', 'o', 'd'], його остаточний пункт ('d') не відображався б у отриманому списку (що цілком чудово з огляду на особливості питання). Це елегантне рішення!
davidchambers

1
@Mark yep (я підтримав), просто вказавши на відмінності (і обмеження, якщо інші люди хочуть іншої поведінки)
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]

Я думаю, що це найбільш пітонічний спосіб зробити це.


3
Чому це не прийнята відповідь? Це найкоротший і найбільш пітонічний і працює з різною довжиною списку!
Jairo Vadillo

5
назва методу zip_longest не izip_longest
Jairo Vadillo

1
Проблема в тому, що значення заповнення за замовчуванням із zip_longest може перезаписати Nones, які * мають бути у списку. Я відредагую в налаштованій версії, щоб це виправити
Dubslow

Примітка: Це може викликати проблеми , якщо ці списки містять елементи зі значенням 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')

Це моя улюблена відповідь. Це так лаконічно.
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

Значення запуску за замовчуванням - 0, і ви не можете зробити 0+ (деякі, кортеж), тому старт змінюється на порожній кортеж.
Зарт

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

6

Якщо обидва списки мають однакову довжину, ви можете зробити:

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

Оскільки перший список має ще один елемент, ви можете додати його post hoc:

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


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

Можливо, трохи пізно придбати ще один пітон-однокласник. Це працює, коли два списки мають однаковий або неоднаковий розмір. Одне, що нічого не варте, це те, що це змінить 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
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__ може бути швидшим.


0

Це неприємно, але працює незалежно від розміру списків:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]


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
Це насправді просто, але воно працює лише зі списками однакової довжини!
Jochen Ritzel

Ви можете виправити це chain.from_iterable(izip(list1, list2), list1[len(list2):])для конкретної заданої тут проблеми ... список1 повинен бути довшим.
Jochen Ritzel

Так, але я волів би запропонувати рішення, яке працює для контейнерів довільної довжини, або поступитися рішенням, запропонованим вище.
Wheaties

-2

Я занадто старий, щоб не дотримуватися розуміння списку, тому:

import operator
list3 = reduce(operator.add, zip(list1, list2))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.