5

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.

Trevor
  • 1,613
  • 3
  • 22
  • 34

4 Answers4

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
utdemir
  • 26,532
  • 10
  • 62
  • 81
  • That's very nice. I was hoping for a simpler solution without the need for a class wrapper, but I'll use this until it appears. Thanks! :) – Trevor Sep 30 '15 at 07:30
2

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
BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • Thanks! This is much closer, but it still feels like there should be a simpler solution that does not require me to even create a second accumulation variable, y. Is there no way to simply pass one generator to the other, scaling the results? I was hoping it would be 1 or 2 lines, beside the 2nd function generator def line. – Trevor Sep 30 '15 at 07:44
  • @Trevor: I don't think you can get a simpler solution if you need to be able to `send` values to `doubler`. When you use `send`, `yield` assumes a dual role, both accepting values sent in, and yielding values back out. If you `send` values in, they will become the result of a `yield` expression in the function body. But you also need `yield` to yield the doubled result back out. So you need to do some work in the function to "communicate" the sent value in and the yielded value out. You also cannot use `yield from` because that doesn't allow you to do your doubling on the way out. – BrenBarn Sep 30 '15 at 08:23
  • @Trevor: Incidentally, you can condense it slightly by combining the second and third lines into `yield next(a)`, which also has the advantage that it will yield the wrapped accumulator's first value (instead of explicitly yielding 0) on the first iteration. – BrenBarn Sep 30 '15 at 08:26
2

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
zehnpaard
  • 6,003
  • 2
  • 25
  • 40
  • Thanks! I used your "even shorter" version, like so: `def doubler(a=accumulator(), y=None): while True: y = yield 2 * a.send(y)` – Trevor Sep 30 '15 at 17:13
  • 1
    One caveat with that approach is that you won't be able to create two doublers just by doing something like `a2 = doubler();a3 = doubler()` as both will try to use the same generator object and the second instance will raise a TypeError. – zehnpaard Sep 30 '15 at 23:33
  • 1
    On the other hand you can do `a2 = doubler();a3 = doubler(accumulator())`, which will have a2 and a3 using different generators. – zehnpaard Sep 30 '15 at 23:37
  • True. In my production version, I instantiated the nested accumulator in the function as opposed to the shared *args location. I only used this "shortest" version for proving concepts. ... Thanks again! ... Example: `def doubler(func_gen, y=None): g = func_gen(); g.send(None); while True: y = yield 2* g.send(y)` – Trevor Oct 01 '15 at 03:14
  • No problem! Just to be a stickler for detail, no need to do `g.send(None)`, as that gets done in the first iteration of the `while True` loop. – zehnpaard Oct 01 '15 at 03:49
0

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.

strubbly
  • 3,347
  • 3
  • 24
  • 36