[Python-ideas] x=(yield from) confusion [was:Yet another alternative name for yield-from]

Jacob Holm jh at improva.dk
Thu Apr 9 16:21:53 CEST 2009


Nick Coghlan wrote:
> Jacob Holm wrote:
>   
>> Then let me revisit my earlier statement that when close() catches a
>> StopIteration with a non-None value, it should either return it or raise
>> an exception.  Since the value is not saved, a second close() will
>> neither be able to return it, nor raise a StopIteration with it. 
>> Therefore I now think that raising a RuntimeError in that case is the
>> only right thing to do.
>>     
>
> Remember, close() is designed to be about finalization. So long as the
> generator indicates that it has finished (i.e. by reraising
> GeneratorExit or raising StopIteration with or without a value), the
> method has done its job. Raising a RuntimeError for a successfully
> closed generator doesn't make any sense.
>   

Returning a value in response to GeneratorExit is what doesn't make any 
sense when there is no possible way to access that value later.  Raising 
a RuntimeError in this case will be a clear reminder of this.  Think 
newbie protection if you will.

> So if someone wants the return value, they'll need to either use next(),
> send() or throw() and catch the StopIteration themselves, or else use
> 'yield from'.
>   

Yes.  How is that an argument against making close raise a RuntimeError 
if it catches a StopIteration with a non-None value?

> That said, creating your own stateful wrapper that preserves the last
> yield value and the final return value of a generator iterator is also
> perfectly possible:
>
>   class CaptureGen(object):
>     """Capture and preserve the last yielded value and the
>        final return value of a generator iterator instance"""
>     NOT_SET = object()
>
>     def __init__(self, geniter):
>       self.geniter = geniter
>       self._last_yield = self.NOT_SET
>       self._return_value = self.NOT_SET
>
>     @property
>     def last_yield(self):
>       if self._last_yield is self.NOT_SET:
>         raise RuntimeError("Generator has not yielded")
>       return self._last_yield
>
>     @property
>     def return_value(self):
>       if self._return_value is self.NOT_SET:
>         raise RuntimeError("Generator has not returned")
>       return self._return_value
>
>     def _delegate(self, meth, *args):
>       try:
>         val = meth(*args)
>       except StopIteration, ex:
>         if self._return_value is self.NOT_SET:
>           self._return_value = ex.value
>           raise
>         raise StopIteration(self._return_value)
>       self._last_yield = val
>       return val
>
>     def __next__(self):
>       return self._delegate(self.geniter.next)
>     next = __next__
>
>     def send(self, val):
>       return self._delegate(self.geniter.send, val)
>
>     def throw(self, et, ev=None, tb=None):
>       return self._delegate(self.geniter.throw, et, ev, tb)
>
>     def close(self):
>       self.geniter.close()
>       return self._return_value
>
> Something like that may actually turn out to be useful as the basis for
> an enhanced coroutine decorator, similar to the way one uses
> contextlib.contextmanager to turn a generator object into a context
> manager. The PEP is quite usable for refactoring without it though.
>
>   

I know it is possible to create a complete workaround.  The problem is 
that any complete workaround will break the chain of yield-from calls, 
causing a massive overhead on next(), send(), and throw() compared to a 
partial workaround that doesn't break the yield-from chain.

> For refactoring, the pattern of passing in a "start" value for use in
> the first yield expression in the subiterator should be adequate. That's
> enough to avoid injecting spurious "None" values into the yield sequence.
>   
If you define "refactoring" narrowly enough, you are probably right.

Otherwise it depends on how you want the new subgenerator to work.  If 
you want it to be a @coroutine (with or without the decorator) there are 
good reasons for wanting to throw away the value from the first next() 
and provide an initial value to send() or throw() immediately after 
that, so the first value yielded by the yield-from becomes the result of 
the send() or throw().  Taking just a simple "start" value in the 
constructor and making the first yield an "x = yield start" doesn't 
support that use.  Taking a compound "start" value in the constructor 
and making the first yield an "x = yield from cr_init(start)" does, by 
skipping the first yield when neccessary and simulating the send() or 
throw().


- Jacob



More information about the Python-ideas mailing list