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

LeafStorm
  • 3,057
  • 4
  • 24
  • 28

6 Answers6

18

For a "clean" implementation, you want

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

but maybe you want

itertools.chain(*itertools.izip_longest(*iters))
  • 2
    Does this exhaust the generator before passing it on to izip and then chain? – Dan Aug 10 '15 at 23:15
  • @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 `` and `(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)`. – Abbafei Feb 16 '17 at 04:43
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.

ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 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 Jan 07 '10 at 02:55
  • The trouble with `zip` in this instance is that it will evaluate the iterators immediately, forcing you to forego generator semantics. – Sapph Jan 07 '10 at 02:56
  • Which would be especially problematic considering that my original use case for this was iterators that generated an infinite number of values. – LeafStorm Jan 07 '10 at 11:29
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))
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
David Jones
  • 4,766
  • 3
  • 32
  • 45
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
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
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
Crast
  • 15,996
  • 5
  • 45
  • 53
2

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]
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237