How to map or nest Python 2.7 function generators?

2019-03-21 03:08发布

问题:

If I have a very simple (although possibly very complex) function generator in Python 2.7, like so:

def accumulator():
    x = yield 0
    while True:
        x += yield x

Which can be used, like so:

>>> a = accumulator()
>>> a.send(None)
0
>>> a.send(1)
1
>>> a.send(2)
3
>>> a.send(3)
6

What would be a simple wrapper for another function generator that produces the same result, except multiplied by 2? The above function generator is simple, but please assume it is too complicated to copy-paste. I'm trying something, like:

def doubler():
    a = accumulator()
    a.send(None)
    y = yield 0
    while True:
        y = 2 * a.send(yield y)

Or, imagining something simpler:

def doubler():
    a = accumulator()
    a.send = lambda v: 2 * super(self).send(v)
    return a

Both of which are horribly broke, so I won't share the syntax errors, but it may illustrate what I'm trying to do.

Ideally, I would like to get something, like:

>>> d = doubler()
>>> d.send(None)
0
>>> d.send(1)
2
>>> d.send(2)
6
>>> d.send(3)
12

The results are the exact same as the original, except doubled.

I'm trying to avoid duplicating a very complicated function generator to create an identical result, except scaled by a known factor.

The second generator will ultimately have a different input stream, so I cannot just use the result from the first generator and double it. I need a second independent generator, wrapping the first.

The input stream is indeterminate, such that it is impossible to generate the entire sequence and then transform.

It seems I want to map or nest these function generators, but I'm not sure of the appropriate jargon, and so I'm getting nowhere in Google.

回答1:

If you need to have the same interface as a coroutine (i.e. have a send method), then BrenBarn's solution is probably as simple as it gets.*

If you can have a slightly different interface, then a higher-order function is even simpler:

def factor_wrapper(coroutine, factor):
    next(coroutine)
    return lambda x, c=coroutine, f=factor: f * c.send(x)

You would use it as follows:

>>> a = accumulator()
>>> a2 = factor_wrapper(a, 2)
>>> print a2(1)
2
>>> print a2(2)
6
>>> print a2(3)
12

*Actually you can shave several lines off to make it 4 lines total, though not really reducing complexity much.

def doubler(a):
    y = yield next(a)
    while True:
        y = yield (2 * a.send(y))

or even shorter...

def doubler(a, y=None):
    while True:
        y = yield 2 * a.send(y)

Either of the above can be used as follows:

>>> a = accumulator()
>>> a2 = doubler(a)
>>> print a2.send(None) # Alternatively next(a2)
0
>>> print a2.send(1)
2
>>> print a2.send(2)
6
>>> print a2.send(3)
12


回答2:

I didn't tried this, but something along these lines:

class Doubler:
  def __init__(self, g):
    self.g = g()

  def __next__(self):
    return self.send(None)

  def send(self, val):
    return self.g.send(val)*2

Also, after Python 3.5, extending this from collections.abc.Container will eliminate the need of __next__, also will make this a proper generator(It currently doesn't support __throw__ etc., but they're just boilerplate).

Edit: Yes, this works:

In [1]: %paste
def accumulator():
    x = yield 0
    while True:
        x += yield x

## -- End pasted text --

In [2]: %paste
class Doubler:
    def __init__(self, g):
        self.g = g()
    def __next__(self):
        return self.send(None)
    def send(self, val):
        return self.g.send(val)*2

## -- End pasted text --

In [3]: d = Doubler(accumulator)

In [4]: d.send(None)
Out[4]: 0

In [5]: d.send(1)
Out[5]: 2

In [6]: d.send(2)
Out[6]: 6

In [7]: d.send(3)
Out[7]: 12


回答3:

You just need to move the yield outside the expression that passes y to a:

def doubler():
    a = accumulator()
    next(a)
    y = yield 0
    while True:
        y = yield (2 * a.send(y))

Then:

>>> a = accumulator()
... d = doubler()
... next(a)
... next(d)
... for i in range(10):
...     print(a.send(i), d.send(i))
0 0
1 2
3 6
6 12
10 20
15 30
21 42
28 56
36 72
45 90


回答4:

I think this is what you want:

def doubler():
    a = accumulator()
    y = a.send(None)
    x = yield 0
    while True:
        y = a.send(x)
        x = yield 2 * y

This completely wraps the accumulator implementation but you could alternatively make that visible and pass it in as a parameter a to doubler.