views:

311

answers:

13

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?

A: 
zip(list1, list2)
leoluk
This doesn't work, zip will create a list of tuples
Jay
This gives [('f', 'hello'), ('o', 'world')] which is incorrect.
davidchambers
[('f', 'hello'), ('o', 'world')] # this isn't what the OP asked for
Tom Anderson
+14  A: 

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

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))
David Zaslavsky
Is there anything that itertools cannot do? +1.
Manoj Govindan
@Manoj flying. You need the antigravity module for that
cobbal
That's rather slick
Basiclife
+8  A: 

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']
Mark Byers
I really liked your initial answer. Though it did not perfectly address the question, it was an elegant way to merge two lists of the same length. I suggest retaining it, along with the lenght caveat, in your current answer.
Paul Sasik
This is similar to David's answer, but is strictly interleaving (it will stop rather than continue with later lists)
cobbal
@cobbal: Isn't that what he wanted?
Mark Byers
This will only work when the lengths of the lists are with 1 of each other. No down vote though as this partial solution is really elegant.
caspin
@caspin: *This will only work when the lengths of the lists are with 1 of each other.* What do you mean? It seems to work fine for any lengths. Can you give an example of where it gives a different result from the OPs code?
Mark Byers
If list1 were instead ['f', 'o', 'o', 'd'], its final item ('d') would not appear in the resulting list (which is totally fine given the specifics of the question). This is an elegant solution!
davidchambers
@Mark yep (I did upvote it), just pointing out the differences (and limitations if other people want different behavior)
cobbal
+1 for solving the stated problem, and for doing it simply too :-) I figured something like this would be possible. Honestly I think the `roundrobin` function is sort of overkill for this situation.
David Zaslavsky
>>> def rr(*iterables):... return (i.next() for i in itertools.cycle(map(iter, iterables)))
Zart
This is a great option, but I've chosen Duncan's approach because of its self-descriptive nature.
davidchambers
+6  A: 

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']
Duncan
Thanks, Duncan. I did not realize that it's possible to specify a step when slicing. What I like about this approach is how naturally it reads.1. Make a list of the correct length.2. Populated the even indexes with the contents of list1.3. Populate the odd indexes with the contents of list2.The fact that the lists are of different lengths is not an issue in this case!
davidchambers
I think it only works when len(list1) - len(list2) is 0 or 1.
xan
If the lists are of appropriate lengths then it works, if not then the original question doesn't specify what answer is expected. It can be easily modified to handle most reasonable situations: for example if you wanted extra elements to be ignored just chop down the longer list before you start; if you want the extra elements interleaved with None then just make sure that result is initialised with some more None's; if you want extra elements just added on the end then do as for ignoring them and then append them.
Duncan
Quite right, Duncan; I like your solution. My previous comment was really directed at davidchambers in stating that lengths weren't an issue. Sorry for not being explicit.
xan
I, too, was unclear. The point I was trying to make is that Duncan's solution, unlike many of those listed, is not complicated by the fact that the lists are of unequal length. Sure, it's applicable in only a limited range of situations, but I'd prefer a really elegant solution that works in this instance to a less elegant solution that works for any two lists.
davidchambers
You can use (2*len(list1)-1) instead of (len(list1)+len(list2)), also i prefer [0::2] instead of [::2].
Lord British
+1 works like proposed, but fails if list2 = ['hello'] ValueError: attempt to assign sequence of size 3 to extended slice of size 2
killown
That should never occur in this particular case, killown. I'm using re.split() which, when used with a capturing group, always returns a list containing an odd number of items. I'm then processing the "evens" and "odds" separately before combining them into a single list using Duncan's technique. http://bitbucket.org/davidchambers/mango/changeset/6ec62ff16207
davidchambers
A: 

I'd do the simple:

chain.from_iterable( izip( list1, list2 ) )

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

wheaties
This doesn't work. It gives `['f', 'hello', 'o', 'world']`.
Mark Byers
It's really simple, but it only works with lists of the same length!
THC4k
So you're right, it does require equal length containers. Hrm...
wheaties
You can fix it with `chain.from_iterable(izip(list1, list2), list1[len(list2):])` for the particular problem asked here ... list1 is supposed to be the longer one.
THC4k
Yeah but I'd rather come up with either a solution that works for arbitrary length containers or yield to the solutions proposed above.
wheaties
A: 

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

import operator
list3 = reduce(operator.add, zip(list1, list2))
Tom Anderson
This gives ('f', 'hello', 'o', 'world') which is incorrect.
davidchambers
A: 
c=[]
for i, j in zip(list1,list2):
    c.append(i)
    c.append(j)
c.append(list1[-1])
JonC
This gives ['f', 'hello', 'o', 'world'] which is incorrect.
davidchambers
edited to append the last element to the result
JonC
A: 

Here's a one liner that does it:

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

Jay
This works correctly but strikes me as inelegant since it's doing so much to achieve something so simple. I'm not saying that this approach is inefficient, simply that it's not particularly easy to read.
davidchambers
A: 

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))]
chernevik
A: 

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)]
Carlos Valiente
A: 

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.

jwp
+1  A: 
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
killown
Be very careful when using mutable default arguments. This will only return the correct answer the first time it's called, as lst will be reused for every call thereafter. This would be better written as lst=None ... if lst is None: lst = [], although I don't see a compelling reason to opt for this approach over others listed here.
davidchambers
the lst is within the function...
killown
@killown - lst is defined within the function so is a local variable. The potential problem is that list1 and list2 are will be reused every time you use the function, even if you call the function with different lists. See http://docs.python.org/tutorial/controlflow.html#default-argument-values
blokeley
@blokeley: wrong, would be reused if it was combine(list1=[...], list2=[...])
killown
When this solution was first posted its first line read `def combine(list1, list2, lst=[]):`, hence my comment. By the time I submitted that comment, though, killown had made the necessary change.
davidchambers
A: 

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

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

Using itertools and assuming that lists don't contain None:

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