Reputation: 358
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
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
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
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