Daniel Murphy
Daniel Murphy

Reputation: 358

Trouble understanding python generators and iterable arguments

I am supposed to write a generator that given a list of iterable arguments produces the 1st element from the 1st argument, 1st element from 2nd argument, 1st element from 3rd element, 2nd element from 1st argument and so on.

So

''.join([v for v in alternate('abcde','fg','hijk')]) == afhbgicjdke

My function works for string arguments like this but I encounter a problem when I try and use a given test case that goes like this

def hide(iterable):
for v in iterable:
    yield v

''.join([v for v in alternate(hide('abcde'),hide('fg'),hide('hijk'))])= afhbgicjdke

Here is my generator:

def alternate(*args):
    for i in range(10):
        for arg in args:
            arg_num = 0
            for thing in arg:
                if arg_num == i:
                    yield thing
                arg_num+=1

Can I change something in this to get it to work as described or is there something fundamentally wrong with my function?

EDIT: as part of the assignment, I am not allowed to use itertools

Upvotes: 0

Views: 1034

Answers (3)

Bashir
Bashir

Reputation: 81

If you want to only pass iterators (this wont work with static string) use the fallowing code :

def alternate(*args):
    for i in range(10):
        for arg in args:
            arg_num = i
            for thing in arg:
                if arg_num == i:
                    yield thing
                    break
                else:
                    arg_num+=1

This is just your original code with a little bit of change . When you are using static string every time that you call alternate function a new string will be passed in and you can start to count from 0 (arg_num = 0).

But when you create iterators by calling hide() method, only one single instance of iterator will be created for each string and you should keep track of your position in the iterators so you have to change arg_num = 0 to arg_num = i and also you need to add the break statement as well .

Upvotes: 0

mgilson
mgilson

Reputation: 309899

Something like this works OK:

def alternate(*iterables):
    iterators = [iter(iterable) for iterable in iterables]

    sentinel = object()

    keep_going = True
    while keep_going:
        keep_going = False
        for iterator in iterators:
            maybe_yield = next(iterator, sentinel)
            if maybe_yield != sentinel:
                keep_going = True
                yield maybe_yield

print ''.join(alternate('abcde','fg','hijk'))

The trick is realizing that when a generator is exhausted, next will return the sentinel value. As long as 1 of the iterators returns a sentinel, then we need to keep going until it is exhausted. If the sentinel was not returned from next, then the value is good and we need to yield it.

Note that if the number of iterables is large, this implementation is sub-optimal (It'd be better to store the iterables in a data-structure that supports O(1) removal and to remove an iterable as soon as it is detected to be exhausted -- a collections.OrderedDict could probably be used for this purpose, but I'll leave that as an exercise for the interested reader).


If we want to open things up to the standard library, itertools can help here too:

from itertools import izip_longest, chain
def alternate2(*iterables):
    sentinel = object()
    result = chain.from_iterable(izip_longest(*iterables, fillvalue=sentinel))
    return (item for item in result if item is not sentinel)

Here, I return a generator expression ... Which is slightly different than writing a generator function, but really not much :-). Again, this can be slightly inefficient if there are a lot of iterables and one of them is much longer than the others (consider the case where you have 100 iterables of length 1 and 1 iterable of length 101 -- This will run in effectively 101 * 101 steps whereas you should really be able to accomplish the iteration in about 101 * 2 + 1 steps).

Upvotes: 3

jsbueno
jsbueno

Reputation: 110271

There are several things that can be improved in your code. What is causing you problems is the most wrong of them all - you are actually iterating several times over each of your arguments - and essentially doing nothing with the intermediate values in each pass.

That takes place when you iterate for thing in arg for each value of i.

While that is a tremendous waste of resources in any account, it also does not work with iterators (which are what you get with your hide function), since they go exhausted after iterating over its elements once - that is in contrast with sequences - that can be iterated - and re-reiterated several ties over (like the strings you are using for test)

(Another wrong thing is to have the 10 hardcoded there as the longest sequence value you'd ever had - in Python you iterate over generators and sequences - don't matter their size)

Anyway, the fix for that is to make sure you iterate over each of your arguments just once - the built-in zip can do that - or for your use case, itertools.zip_longest(izip_longest in Python 2.x) can retrieve the values you want from your args in a single for structure:

from itertools import izip_longest
def alternate(*args):
    sentinel = object()
    for values in izip_longest(*args, fillvalue=sentinel):
         for value in values:
              if value is not sentinel:
                   yield value

Upvotes: 0

Related Questions