views:

145

answers:

3

Lately, I tried to set local variables from outside of a running generator. The generator code also should access these variables.

One trouble was, that when accessing the variables, it seamed that the interpreter was thinking it must be a global since the variable was not set in the local scope. But I don't wanted to change the global variables and did also not want to copy the whole global scope to make the variables artificially local.

An other trouble was, that it seams that the dictionaries for locals (and globals?) seamed to be read-only when accessed from outside.

Is there any legal (or at least partial legal way) to introduce locals into a running generator instance?

Edit for clarification:

I don't mean the "send" function. This is of course a neat function, but since I want to set multiple variables with differing names, it is not conveniant for my purposes.

A: 

locals() always returns a read-only dict. You could create your own "locals" dictionary:

def gen_func():
    lcls = {}
    for i in range(5):
        yield (i, lcls)
        print lcls


for (val, lcls) in gen_func():
    lcls[val] = val

Any other mutable structure will also work.

oggy
+4  A: 

What you may be looking for, is the send method, which allows a value to be sent into a generator. The reference provides an example:

>>> def echo(value=None):
...     print "Execution starts when 'next()' is called for the first time."
...     try:
...         while True:
...             try:
...                 value = (yield value)
...             except Exception, e:
...                 value = e
...     finally:
...         print "Don't forget to clean up when 'close()' is called."
...
>>> generator = echo(1)
>>> print generator.next()
Execution starts when 'next()' is called for the first time.
1
>>> print generator.next()
None
>>> print generator.send(2)
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.


Let me give an example of my own. (Watch out! The code above is Python 2.6, but below I'll write Python 3; py3k ref):

>>> def amplify(iter, amp=1):
...     for i in iter:
...         reply = (yield i * amp)
...         amp = reply if reply != None else amp 
... 
>>> it = amplify(range(10))
>>> next(it)
0
>>> next(it)
1
>>> it.send(3) # 2 * 3 = 6
6
>>> it.send(8) # 3 * 8 = 24
24
>>> next(it) # 4 * 8 = 32
32

Of course, if your really want to, you can also do this without send. E.g. by encapsulating the generator inside a class (but it's not nearly as elegant!):

>>> class MyIter:
...     def __init__(self, iter, amp=1):
...         self.iter = iter
...         self.amp = amp
...     def __iter__(self):
...         for i in self.iter:
...             yield i * self.amp
...     def __call__(self):
...         return iter(self)
... 
>>> iterable = MyIter(range(10))
>>> iterator = iterable()
>>> next(iterator)
0
>>> next(iterator)
1
>>> iterable.amp = 3
>>> next(iterator)
6
>>> iterable.amp = 8
>>> next(iterator)
24
>>> next(iterator)
32


Update: Alright, now that you have updated your question, let me have another stab at the problem. Perhaps this is what you mean?

>>> def amplify(iter, loc={}):
...     for i in iter:
...         yield i * loc.get('amp', 1)
... 
>>> it = amplify(range(10), locals())
>>> next(it)
0
>>> next(it)
1
>>> amp = 3
>>> next(it)
6
>>> amp = 8
>>> next(it)
24
>>> next(it)
32

Note that locals() should be treated as read-only and is scope dependent. As you can see, you'll need to explicitly pass locals() to the generator. I see no way around this...

Stephan202
Thanks for the really long examples. It seams, I forgot to add a comment about "send" here in the first edit of my question.
Juergen
Alright, I now added another version with `locals()`. Perhaps this suits your needs?
Stephan202
+1  A: 

If you want to have a coroutine or a generator that also acts as a sink, you should use the send method, as in Stephan202's answers. If you want to change the runtime behavior by settings various attributes in the generator, there's an old recipe by Raymond Hettinger:

def foo_iter(self):
    self.v = "foo"
    while True:
        yield self.v

enableAttributes(foo_iter)
it = foo_iter()
print it.next()
it.v = "boo"
print it.next()

This will print:

foo
boo

It shouldn't be too difficult to convert the enableAttributes function into a proper decorator.

Torsten Marek
Hi Torsten, thanks, this comes really close to what I wanted to do. I did not know about that recipe and it seams really very interesting to me (have to examine closer in a silent hour). Still it would be nice to have something more conveniant inside the generator-coding ...
Juergen