9

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

3

6 Answers 6

18

For a "clean" implementation, you want

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

but maybe you want

itertools.chain(*itertools.izip_longest(*iters))
Sign up to request clarification or add additional context in comments.

2 Comments

Does this exhaust the generator before passing it on to izip and then chain?
@Dan If iters was a generator, it would exhaust it, at least in python 2; running def z(*iters): print(type(iters)); print(iters) then z(*(i for i in xrange(1,18))) prints <type 'tuple'> and (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17).
8

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.

3 Comments

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!
The trouble with zip in this instance is that it will evaluate the iterators immediately, forcing you to forego generator semantics.
Which would be especially problematic considering that my original use case for this was iterators that generated an infinite number of values.
7

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

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

Comments

3

There are two problems with your attempt:

  1. You don't wrap each object in iters with iter() so it will fail with iterables such as list; and
  2. By passing on StopIteration your generator is an infinite loop.

Some simple code that does solves both those issues and is still easy to read and understand:

def alternate(*iters):
    iters = [iter(i) for i in iters]
    while True:
        for i in iters:
            yield next(i)

>>> list(alternate(range(1, 7, 2), range(2, 8, 2)))
[1, 2, 3, 4, 5, 6]

Comments

2

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

Comments

2

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

1 Comment

Might be cleaner to define Dummy as Dummy = object()

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.