generator functions: why won't this work?

Steve Holden steve at holdenweb.com
Wed Apr 2 09:59:17 EDT 2008


zillow10 at googlemail.com wrote:
> On Apr 2, 4:42 am, "Gabriel Genellina" <gagsl-... at yahoo.com.ar> wrote:
>> En Tue, 01 Apr 2008 23:56:50 -0300, <zillo... at googlemail.com> escribió:
>>
>>> I'm trying to understand generator functions and the yield keyword.
>>> I'd like to understand why the following code isn't supposed to work.
>>> (What I would have expected it to do is, for a variable number of
>>> arguments composed of numbers, tuples of numbers, tuples of tuples,
>>> etc., the function would give me the next number "in sequence")
>>> ####################################
>>> def getNextScalar(*args):
>>>    for arg in args:
>>>       if ( isinstance(arg, tuple)):
>>>          getNextScalar(arg)
>>>       else:
>>>          yield arg
>>> ####################################
>> You're not the first one in getting confused. After all, this schema works
>> well for other recursive constructs.
>> Perhaps a progression of working code samples will help to understand what
>> happens here. The simplest thing would be to just print the items as
>> they're encountered, in a recursive call:
>>
>> py> data = (1, 2, (3,4,(5,6),7))
>> py>
>> py> print "1) using print"
>> 1) using print
>> py>
>> py> def getNextScalar(args):
>> ...    for arg in args:
>> ...       if isinstance(arg, tuple):
>> ...          getNextScalar(arg)
>> ...       else:
>> ...          print arg
>> ...
>> py> getNextScalar(data)
>> 1
>> 2
>> 3
>> 4
>> 5
>> 6
>> 7
>>
>> Now one could try to collect the numbers in a list:
>>
>> py> print "2) using extend"
>> 2) using extend
>> py>
>> py> def getNextScalar(args):
>> ...    result = []
>> ...    for arg in args:
>> ...       if isinstance(arg, tuple):
>> ...          result.extend(getNextScalar(arg))
>> ...       else:
>> ...          result.append(arg)
>> ...    return result
>> ...
>> py> getNextScalar(data)
>> [1, 2, 3, 4, 5, 6, 7]
>>
>> Note that we use two different list methods: for individual items, we use
>> "append", but for tuples we use "extend" in the recursive call. If extend
>> weren't available, we could emulate it with append:
>>
>> py> print "3) using append"
>> 3) using append
>> py>
>> py> def getNextScalar(args):
>> ...    result = []
>> ...    for arg in args:
>> ...       if isinstance(arg, tuple):
>> ...          for item in getNextScalar(arg):
>> ...              result.append(item)
>> ...       else:
>> ...          result.append(arg)
>> ...    return result
>> ...
>> py> getNextScalar(data)
>> [1, 2, 3, 4, 5, 6, 7]
>>
>> See how we need an additional loop to iterate over the results that we get
>>  from the recursive call.
>> Now instead of building an intermediate result list, we delegate such task
>> over the caller, and we use a generator that just yields items; this way,
>> we remove all references to the result list and all result.append calls
>> become yield statements. The inner loop has to remain the same. The yield
>> statement acts somewhat as an "append" over an outer list created by the
>> generator's caller.
>>
>> py> print "4) using yield"
>> 4) using yield
>> py>
>> py> def getNextScalar(args):
>> ...    for arg in args:
>> ...       if isinstance(arg, tuple):
>> ...          for item in getNextScalar(arg):
>> ...              yield item
>> ...       else:
>> ...          yield arg
>> ...
>> py> getNextScalar(data)
>> <generator object at 0x00A3AE68>
>> py> list(getNextScalar(data))
>> [1, 2, 3, 4, 5, 6, 7]
>>
>> I hope it's more clear now why you have to use yield on the recursive call
>> too.
>>
>> <idea mode="raw">
>> Perhaps this:
>>
>>    yield *iterable
>>
>> could be used as a shortcut for this:
>>
>>    for __temp in iterable: yield __temp
>>
>> </idea>
>>
>> --
>> Gabriel Genellina
> 
> Thanks to everyone for your very helpful replies. I think I was trying
> to use generator functions without first having really read about
> iterators (something I still haven't done, although I've managed to
> extrapolate some details based on your comments), and therefore really
> "putting the cart before the horse". I was interpreting
> getNextScalar(arg) on line 3 as a function call in the usual sense
> rather than an object that was being created but never used.
> 
> I have a question about the other issue that was pointed out. With
> reference to the following (corrected) code:
> 
> def getNextScalar(*args):
> 	for arg in args:
> 		if(isinstance(arg, tuple)):
> 			for f in getNextScalar(*arg): # why not getNextScalar(arg)?
> 				yield f
> 		else:
> 			yield arg
> 
> although I've verified that the line 4 needs to be "for f in
> getNextScalar(*arg)" rather than "for f in getNextScalar(arg)" when
> passing multiple arguments to the function, I'd just like to test my
> understanding of this. Suppose I create the following generator
> object:
> 
> g = getNextScalar(1, 2, (3, 4), 5)
> 
> when the iterator reaches the tuple argument (3, 4) then, according to
> Steve and George, the * in *arg causes this tuple to be expanded into
> positional arguments, and it makes sense to do it this way. But what
> happens when getNextScalar(arg) is used instead? I presume that (3,4)
> is passed as a single tuple argument, if(isinstance(arg, tuple)) is
> true, and the tuple is passed again as an the argument to
> getNextScalar()... ad infinitum. The alternative is to define a
> function that takes a tuple, as Gabriel has done.
> 
Either approach would work - it just seemed to make more sense to use 
existing functionality and recurse (at least to me). You should try 
removing the * from the recursive call. I expect you would find that you 
(eventually) experience a stack overflow due to the infinite nature of 
the recursion.

regards
  Steve
-- 
Steve Holden        +1 571 484 6266   +1 800 494 3119
Holden Web LLC              http://www.holdenweb.com/




More information about the Python-list mailing list