Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map or nest Python 2.7 function generators?

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.

like image 203
Trevor Avatar asked Sep 30 '15 07:09

Trevor


People also ask

Is map in python a generator?

map() returns a map object, which is an iterator that yields items on demand. So, the natural replacement for map() is a generator expression because generator expressions return generator objects, which are also iterators that yield items on demand.

How to define a generator in Python?

It is fairly simple to create a generator in Python. It is as easy as defining a normal function, but with a yield statement instead of a return statement. If a function contains at least one yield statement (it may contain other yield or return statements), it becomes a generator function.

How to use yield and generator in Python?

You can assign this generator to a variable in order to use it. When you call special methods on the generator, such as next() , the code within the function is executed up to yield . When the Python yield statement is hit, the program suspends function execution and returns the yielded value to the caller.


2 Answers

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
like image 87
zehnpaard Avatar answered Sep 20 '22 14:09

zehnpaard


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
like image 32
utdemir Avatar answered Sep 20 '22 14:09

utdemir