Converting “yield from” statement to Python 2.7 code

僤鯓⒐⒋嵵緔 提交于 2019-11-26 12:21:38
abarnert

If you don't use the results of your yields,* you can always turn this:

yield from foo

… into this:

for bar in foo:
    yield bar

There might be a performance cost,** but there is never a semantic difference.


Are the entries from the two halves (upper and lower) appended to each other in one list so that the parent recursion function with the yield from call and use both the halves together?

No! The whole point of iterators and generators is that you don't build actual lists and append them together.

But the effect is similar: you just yield from one, then yield from another.

If you think of the upper half and the lower half as "lazy lists", then yes, you can think of this as a "lazy append" that creates a larger "lazy list". And if you call list on the result of the parent function, you of course will get an actual list that's equivalent to appending together the two lists you would have gotten if you'd done yield list(…) instead of yield from ….

But I think it's easier to think of it the other way around: What it does is exactly the same the for loops do.

If you saved the two iterators into variables, and looped over itertools.chain(upper, lower), that would be the same as looping over the first and then looping over the second, right? No difference here. In fact, you could implement chain as just:

for arg in *args:
    yield from arg

* Not the values the generator yields to its caller, the value of the yield expressions themselves, within the generator (which come from the caller using the send method), as described in PEP 342. You're not using these in your examples. And I'm willing to bet you're not in your real code. But coroutine-style code often uses the value of a yield from expression—see PEP 3156 for examples. Such code usually depends on other features of Python 3.3 generators—in particular, the new StopIteration.value from the same PEP 380 that introduced yield from—so it will have to be rewritten. But if not, you can use the PEP also shows you the complete horrid messy equivalent, and you can of course pare down the parts you don't care about. And if you don't use the value of the expression, it pares down to the two lines above.

** Not a huge one, and there's nothing you can do about it short of using Python 3.3 or completely restructuring your code. It's exactly the same case as translating list comprehensions to Python 1.5 loops, or any other case when there's a new optimization in version X.Y and you need to use an older version.

Replace them with for-loops:

yield from range(L[start] + 1, L[end])

==>

for i in range(L[start] + 1, L[end]):
    yield i

The same about elements:

yield from missing_elements(L, index, end)

==>

for el in missing_elements(L, index, end):
    yield el

I just came across this issue and my usage was a bit more difficult since I needed the return value of yield from:

result = yield from other_gen()

This cannot be represented as a simple for loop but can be reproduced with this:

_iter = iter(other_gen())
try:
    while True: #broken by StopIteration
        yield next(_iter)
except StopIteration as e:
    if e.args:
        result = e.args[0]
    else:
        result = None

Hopefully this will help people who come across the same problem. :)

I think I found a way to emulate Python 3.x yield from construct in Python 2.x. It's not efficient and it is a little hacky, but here it is:

import types

def inline_generators(fn):
    def inline(value):
        if isinstance(value, InlineGenerator):
            for x in value.wrapped:
                for y in inline(x):
                    yield y
        else:
            yield value
    def wrapped(*args, **kwargs):
        result = fn(*args, **kwargs)
        if isinstance(result, types.GeneratorType):
            result = inline(_from(result))
        return result
    return wrapped

class InlineGenerator(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

def _from(value):
    assert isinstance(value, types.GeneratorType)
    return InlineGenerator(value)

Usage:

@inline_generators
def outer(x):
    def inner_inner(x):
        for x in range(1, x + 1):
            yield x
    def inner(x):
        for x in range(1, x + 1):
            yield _from(inner_inner(x))
    for x in range(1, x + 1):
        yield _from(inner(x))

for x in outer(3):
    print x,

Produces output:

1 1 1 2 1 1 2 1 2 3

Maybe someone finds this helpful.

Known issues: Lacks support for send() and various corner cases described in PEP 380. These could be added and I will edit my entry once I get it working.

Cliff Hill

What about using the definition from pep-380 in order to construct a Python 2 syntax version:

The statement:

RESULT = yield from EXPR

is semantically equivalent to:

_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

In a generator, the statement:

return value

is semantically equivalent to

raise StopIteration(value)

except that, as currently, the exception cannot be caught by except clauses within the returning generator.

The StopIteration exception behaves as though defined thusly:

class StopIteration(Exception):

    def __init__(self, *args):
        if len(args) > 0:
            self.value = args[0]
        else:
            self.value = None
        Exception.__init__(self, *args)

I've found using resource contexts (using the python-resources module) to be an elegant mechanism for implementing subgenerators in Python 2.7. Conveniently I'd already been using the resource contexts anyway.

If in Python 3.3 you would have:

@resources.register_func
def get_a_thing(type_of_thing):
    if type_of_thing is "A":
        yield from complicated_logic_for_handling_a()
    else:
        yield from complicated_logic_for_handling_b()

def complicated_logic_for_handling_a():
    a = expensive_setup_for_a()
    yield a
    expensive_tear_down_for_a()

def complicated_logic_for_handling_b():
    b = expensive_setup_for_b()
    yield b
    expensive_tear_down_for_b()

In Python 2.7 you would have:

@resources.register_func
def get_a_thing(type_of_thing):
    if type_of_thing is "A":
        with resources.complicated_logic_for_handling_a_ctx() as a:
            yield a
    else:
        with resources.complicated_logic_for_handling_b_ctx() as b:
            yield b

@resources.register_func
def complicated_logic_for_handling_a():
    a = expensive_setup_for_a()
    yield a
    expensive_tear_down_for_a()

@resources.register_func
def complicated_logic_for_handling_b():
    b = expensive_setup_for_b()
    yield b
    expensive_tear_down_for_b()

Note how the complicated-logic operations only require the registration as a resource.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!