Juergen
Juergen

Reputation: 12728

Add local variable to running generator

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.

Upvotes: 1

Views: 2620

Answers (3)

Stephan202
Stephan202

Reputation: 61549

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...

Upvotes: 5

Torsten Marek
Torsten Marek

Reputation: 86542

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.

Upvotes: 1

oggy
oggy

Reputation: 3571

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.

Upvotes: 1

Related Questions