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

Jacob Holm jh at improva.dk
Wed Apr 8 03:59:19 CEST 2009


Nick Coghlan wrote:
> Jacob Holm wrote:
>   
>> So one @coroutine can't call another using yield-from. Why shouldn't it
>> be possible? All we need is a way to avoid the first next() call and
>> substitute some other value.
>>
>> [snip code]
> You can fix this without syntax by changing the way avg2 is written.
>
> [snip code]
>
> So it just becomes a new rule of thumb for coroutines: a yield-from
> friendly coroutine will accept a "start" argument that defaults to None
> and is returned from the first yield call.
>   

That is not quite enough.  Your suggested rule of thumb is to replace

def example(*args, **kw):
    ...
    x = yield   # first yield
 


with

def example(start=None, *args, **kw):
    ...
    x = yield start  # first yield


That would have been correct if my statement about what was needed 
wasn't missing a bit.  What you actually need to replace with is 
something like:

def example(start, *args, **kw):
    ...
    if 'throw' in start:
        raise start['throw'] # simulate a throw() on first next()
    elif 'send' in start:
        x = start['send']    # simulate a send() on first next()
    else:
        x = yield start.get('yield')  # use specified value for first next()


This allows you to set up so the first next() call skips the yield and 
acts like a send() or a throw() was called.  Actually, I think that can 
be refactored to:

def cr_init(start):
    if 'throw' in start:
        raise start['throw']
    if 'send' in start:
        return start['send']
    return yield start.get('yield')

def example(start, *args, **kw):
    ...
    x = yield from cr_init(start)


Which makes it almost bearable.

It is also possible to write a @coroutine decorator that can be used 
with this, the trick is to make the undecorated function available as an 
attribute of the wrapper so it is available for use in yield-from.  The 
wrapper can also hide the existence of the start argument from top-level 
users.

def coroutine(func):
    def start(*args, **kwargs):
        cr = func({}, *args, **kwargs)
        v = cr.next()
        if v is not None:
            raise RuntimeError('first yield from coroutine was not None')
        return cr
    start.raw = func
    return start


Using such a coroutine in yield-from then becomes:

# Yield None as first value
yield from example.raw({}, *args, **kwargs)

# Yield 42 as first value
yield from example.raw({'yield':42}, *args, **kwargs)

# Skip the first yield and treat the first next() as a send(42)
yield from example.raw({'send':42},  *args, **kwargs)

# Skip the first yield and treat the first next() as a throw(ValueError(42))
yield from example.raw({'throw':ValueError(42)}, *args, **kwargs)


While using it in other contexts is exactly like people are used to.

So it turns out a couple of support routines and a simple convention can 
work around most of the problems with using @coroutines in yield-from.   
I still think it would be nice if yield-from didn't insist on treating 
its iterator as if it was new.


- Jacob




More information about the Python-ideas mailing list