views:

199

answers:

5

What is the most efficient way to alternate taking values from different iterators in Python, so that, for example, alternate(xrange(1, 7, 2), xrange(2, 8, 2)) would yield 1, 2, 3, 4, 5, 6. I know one way to implement it would be:

def alternate(*iters):
    while True:
        for i in iters:
            try:
                yield i.next()
            except StopIteration:
                pass

But is there a more efficient or cleaner way? (Or, better yet, an itertools function I missed?)

+5  A: 

what about zip? you may also try izip from itertools

>>> zip(xrange(1, 7, 2),xrange(2, 8 , 2))
[(1, 2), (3, 4), (5, 6)]

if this is not what you want, please give more examples in your question post.

ghostdog74
On its own, izip isn't enough for my purposes. But putting a chain around izip, like `chain.from_iterable(izip(iterables))` does work. I guess that's the cool thing about iterables. Thanks!
LeafStorm
The trouble with `zip` in this instance is that it will evaluate the iterators immediately, forcing you to forego generator semantics.
Sapph
Which would be especially problematic considering that my original use case for this was iterators that generated an infinite number of values.
LeafStorm
+1  A: 

You could define alternate like this:

import itertools
def alternate(*iters):   
    for elt in itertools.chain.from_iterable(
        itertools.izip(*iters)):
        yield elt

print list(alternate(xrange(1, 7, 2), xrange(2, 8, 2)))

This leaves open the question of what to do if one iterator stops before another. If you'd like to continue until the longest iterator is exhausted, then you could use itertools.izip_longest in place of itertools.izip.

import itertools
def alternate(*iters):   
    for elt in itertools.chain.from_iterable(
        itertools.izip_longest(*iters)):
        yield elt
print list(alternate(xrange(1, 7, 2), xrange(2, 10, 2)))

This will put yield

[1, 2, 3, 4, 5, 6, None, 8]

Note None is yielded when the iterator xrange(1,7,2) raises StopIteration (has no more elements).

If you'd like to just skip the iterator instead of yielding None, you could do this:

Dummy=object()

def alternate(*iters):   
    for elt in itertools.chain.from_iterable(
        itertools.izip_longest(*iters,fillvalue=Dummy)):
        if elt is not Dummy:
            yield elt
unutbu
Might be cleaner to define Dummy as `Dummy = object()`
Chris Lutz
@Chris: Thanks! I made the change.
unutbu
+6  A: 

For a "clean" implementation, you want

itertools.chain(*itertools.izip(*iters))

but maybe you want

itertools.chain(*itertools.izip_longest(*iters))
Sam Hughes
+1  A: 

If they're the same length, itertools.izip can be leveraged like so:

def alternate(*iters):
    for row in itertools.izip(*iters):
       for i in row:
           yield i
Crast
+1  A: 

See roundrobin in the itertools "Recipes" section. It's a more general version of alternate.

David Jones