Home > OS >  Continue to other generators once a generator has been exhausted in a list of generators?
Continue to other generators once a generator has been exhausted in a list of generators?

Time:10-28

I have a list of generators in a function alternate_all(*args) that alternates between each generator in the list to print their first item, second item, ..., etc. until all generators are exhausted.

My code works until a generator is exhausted and once the StopIteration occurs, it stops printing, when I want it to continue with the rest of the generators and ignore the exhausted one:

def alternate_all(*args):
    iter_list = []
    for iterable in args:
        iter_list.append(iter(iterable))
    try:
        while True:
            for iterable in iter_list:
                val = next(iter_list[0])
                iter_list.append(iter_list.pop(0))
                yield val
    except StopIteration:
        pass

            
if __name__ == '__main__':
    for i in alternate_all('abcde','fg','hijk'):
        print(i,end='')

My output is:

afhbgic

When it should be:

afhbgicjdke

How can I get this to ignore the exhausted generator? I would prefer not to use itertools and keep this same structure.

CodePudding user response:

This seems to work as expected:

def alternating_iterators(*args):
    i = 0
    n = len(args)
    iters = list(map(iter, args))
    while iters:
        try:
            yield next(iters[i % n])
            i  = 1
        except StopIteration:
            iters.pop(i % n)
            n -= 1

Output:

>>> "".join(alternating_iterators("abcde", "fg", "hijk"))
'afhbgicjdke'

CodePudding user response:

This doesn't really answer your question, but here's an abbreviated version using itertools.zip_longest that I was able to test with. It seems to give the desired output as mentioned above.

from itertools import zip_longest


def alternate_all(*args):
    return ''.join([elem for L in zip_longest(*args, fillvalue='') for elem in L])


print(alternate_all('abcde','fg','hijk'))

CodePudding user response:

How about the itertools-recipe roundrobin, straight from the itertools documentation? You'd still end up using itertools.cycle and itertools.islice, though, not sure if that's a deal-breaker.

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            # Remove the iterator we just exhausted from the cycle.
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))

CodePudding user response:

This works. I tried to stay close to how your original code works (though I did replace your first loop with a list comprehension for simplicity).

def alternate_all(*args):
    iter_list = [iter(arg) for arg in args]
    while iter_list:
        i = iter_list.pop(0)
        try:
            val = next(i)
        except StopIteration:
            pass
        else:
            yield val
            iter_list.append(i)

The main problem with your code was that your try/except was outside of the loop, meaning the first exhausted iterator would exit from the loop. Instead, you want to catch StopException inside the loop so you can keep going, and the loop should keep going while iter_list still has any iterators in it.

  • Related