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

Jacob Holm jh at improva.dk
Fri Apr 10 20:58:37 CEST 2009


Greg Ewing wrote:
> Jacob Holm wrote:
>> I am saying that there are examples where it is desirable to move one 
>> of the arguments that this form of refactoring forces you to put in 
>> the constructor so it instead becomes the argument of the first send.
>
> I'm having trouble seeing circumstances in which you
> would need to do that. Can you provide an example
> in the form of
>
> (a) a piece of unfactored code

Ok, once again based on your own parser example.   The parse_items 
generator could have been written as:

def parse_items(closing_tag = None):
    elems = []
    token = yield
    while token != closing_tag:
        if is_opening_tag(token):
            name = token[1:-1]
            items = yield from parse_items("</%s>" % name)
            elems.append((name, items))
        else:
            elems.append(token)
        token = yield
    return elems


>
> (b) a desired refactoring

I would like to split off a function for parsing a single element.  And 
I would like it to look like this:

def parse_elem():
    opening_tag = yield
    name = opening_tag[1:-1]
    items = yield from parse_items("</%s>" % name)
    return (name, items)


This differs from the version in your example by taking all the tags as 
arguments to send() instead of having the opening tag as an argument to 
the constructor.

Unfortunately, there is no way to actually use this version in the 
implementation of parse_items.

>
> (c) an explanation of why the desired refactoring
>     can't conveniently be done using an unprimed
>     generator and plain yield-from.
>

The suggested subroutine cannot be used, because parse_items already has 
the value that should go as the argument to the first send().

It is easy to rewrite it to the version you used in the example, but 
that requires you to make the opening_tag an argument to the 
constructor, whereas I want it as an argument to the first send.  You 
can of course make that argument optional and adjust the function to 
only do the first yield if the argument is not given.  That is 
essentially what my "cr_init()" pattern does.  Using that pattern, the 
refactoring looks like this:

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

def parse_elem(start=None):
    opening_tag = yield from cr_init(start)
    name = opening_tag[1:-1]
    items = yield from parse_items("</%s>" % name)
    return (name, items)

def parse_items(closing_tag=None, start=None):
    elems = []
    token = yield from cr_init(start)
    while token != closing_tag:
        if is_opening_tag(token):
            elems.append(yield from parse_elem(start={'send':token}))
        else:
            elems.append(token)
        token = yield
    return elems


As you see it *can* be done, but I would hardly call it convenient.  The 
main problem is that the coroutine you want to call must be written with 
this in mind or you are out of luck.  While it *is* possible to write a 
wrapper that lets you call the unmodified parse_elem, that wrapper 
cannot use yield_from to call it so you get a rather large overhead that 
way.

A convention like Nick suggested where all coroutines take an optional 
"start" argument with the first value to yield doesn't help, because it 
is not the value to yield that is the problem.

I hope this helps to explain why the cr_init pattern is needed even for 
relatively simple refactoring now that it seems we are not fixing the 
"initial next()" issue.

- Jacob



More information about the Python-ideas mailing list