简体   繁体   中英

Pythonic way to combine (interleave, interlace, intertwine) two lists in an alternating fashion?

I have two lists, the first of which is guaranteed to contain exactly one more item than the second . I would like to know the most Pythonic way to create a new list whose even-index values come from the first list and whose odd-index values come from the second list.

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

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

This works, but isn't pretty:

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

How else can this be achieved? What's the most Pythonic approach?


If you need to handle lists of mismatched length (eg the second list is longer, or the first has more than one element more than the second), some solutions here will work while others will require adjustment. For more specific answers, see How to interleave two lists of different length? to leave the excess elements at the end, or How to elegantly interleave two lists of uneven length in python? to try to intersperse elements evenly.

Here's one way to do it by slicing:

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

There's a recipe for this in the itertools documentation :

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

EDIT:

For python's version greater than 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))
import itertools
print [x for x in itertools.chain.from_iterable(itertools.izip_longest(list1,list2)) if x]

I think this is the most pythonic way of doing it.

In Python 2, this should do what you want:

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

Without itertools and assuming l1 is 1 item longer than l2:

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

In python 2, using itertools and assuming that lists don't contain None:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')

I know the questions asks about two lists with one having one item more than the other, but I figured I would put this for others who may find this question.

Here is Duncan's solution adapted to work with two lists of different sizes.

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

This outputs:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 

If both lists have equal length, you can do:

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

As the first list has one more element, you can add it post hoc:

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

这是一个这样做的班轮:

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

This one is based on Carlos Valiente's contribution above with an option to alternate groups of multiple items and make sure that all items are present in the output :

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

This will interlace lists A and B by groups of 3 values each:

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

Here's a one liner using list comprehensions, w/o other libraries:

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

Here is another approach, if you allow alteration of your initial list1 by side effect:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]

Might be a bit late buy yet another python one-liner. This works when the two lists have equal or unequal size. One thing worth nothing is it will modify a and b. If it's an issue, you need to use other solutions.

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

My take:

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)]
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
from itertools import chain
list(chain(*zip('abc', 'def')))  # Note: this only works for lists of equal length
['a', 'd', 'b', 'e', 'c', 'f']

Stops on the shortest:

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

Sure, resolving the next/__next__ method may be faster.

Multiple one-liners inspired by answers to another question :

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]

How about numpy? It works with strings as well:

import numpy as np

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

Result:

array([1, 2, 2, 3, 3, 4])

An alternative in a functional & immutable way (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),[])

itertools.zip_longest returns an iterator of tuple pairs with any missing elements in one list replaced with fillvalue=None (passing fillvalue=object lets you use None as a value). If you flatten these pairs, then filter fillvalue in a list comprehension, this gives:

>>> from itertools import zip_longest
>>> def merge(a, b):
...     return [
...         x for y in zip_longest(a, b, fillvalue=object)
...         for x in y if x is not object
...     ]
...
>>> merge("abc", "defgh")
['a', 'd', 'b', 'e', 'c', 'f', 'g', 'h']
>>> merge([0, 1, 2], [4])
[0, 4, 1, 2]
>>> merge([0, 1, 2], [4, 5, 6, 7, 8])
[0, 4, 1, 5, 2, 6, 7, 8]

Generalized to arbitrary iterables:

>>> def merge(*its):
...     return [
...         x for y in zip_longest(*its, fillvalue=object)
...         for x in y if x is not object
...     ]
...
>>> merge("abc", "lmn1234", "xyz9", [None])
['a', 'l', 'x', None, 'b', 'm', 'y', 'c', 'n', 'z', '1', '9', '2', '3', '4']
>>> merge(*["abc", "x"]) # unpack an iterable
['a', 'x', 'b', 'c']

Finally, you may want to return a generator rather than a list comprehension:

>>> def merge(*its):
...     return (
...         x for y in zip_longest(*its, fillvalue=object)
...         for x in y if x is not object
...     )
...
>>> merge([1], [], [2, 3, 4])
<generator object merge.<locals>.<genexpr> at 0x000001996B466740>
>>> next(merge([1], [], [2, 3, 4]))
1
>>> list(merge([1], [], [2, 3, 4]))
[1, 2, 3, 4]

If you're OK with other packages, you can try more_itertools.roundrobin :

>>> list(roundrobin('ABC', 'D', 'EF'))
['A', 'D', 'E', 'B', 'F', 'C']

using for loop also we can achive this easily:

list1 = ['f', 'o', 'o']
list2 = ['hello', 'world']
list3 = []

for i in range(len(list1)):
    #print(list3)
    list3.append(list1[i])
    if i < len(list2):
        list3.append(list2[i])
        
print(list3)

output :

['f', 'hello', 'o', 'world', 'o']

Further by using list comprehension this can be reduced. But for understanding this loop can be used.

Obviously late to the party, but here's a concise one for equal-length lists:

output = [e for sub in zip(list1,list2) for e in sub]

It generalizes for an arbitrary number of equal-length lists, too:

output = [e for sub in zip(list1,list2,list3) for e in sub]

etc.

My approach looks as follows:

from itertools import chain, zip_longest

def intersperse(*iterators):
    # A random object not occurring in the iterators
    filler = object()

    r = (x for x in chain.from_iterable(zip_longest(*iterators, fillvalue=filler)) if x is not filler)

    return r

list1 = ['f', 'o', 'o']
list2 = ['hello', 'world']

print(list(intersperse(list1, list2)))

It works for an arbitrary number of iterators and yields an iterator, so I applied list() in the print line.

def alternate_elements(small_list, big_list):
mew = []
count = 0
for i in range(len(small_list)):
    mew.append(small_list[i])
    mew.append(big_list[i])
    count +=1
return mew+big_list[count:]

if len(l2)>len(l1):
    res = alternate_elements(l1,l2)
else:
    res = alternate_elements(l2,l1)

print(res)

Here we swap lists based on size and perform, can someone provide better solution with time complexity O(len(l1)+len(l2))

This is nasty but works no matter the size of the lists:

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

I'd do the simple:

chain.from_iterable( izip( list1, list2 ) )

It'll come up with an iterator without creating any additional storage needs.

I'm too old to be down with list comprehensions, so:

import operator
list3 = reduce(operator.add, zip(list1, list2))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM