Context manager to temporarily change the variable of a register [akawrite swap(a,b)]

E

Evan Driscoll

(If you don't want to read the following, note that you can answer my
question by writing a swap function.)

I want to make a context manager that will temporarily change the
value of a variable within the scope of a 'with' that uses it. This is
inspired by a C++ RAII object I've used in a few projects. Ideally,
what I want is something like the following:

x = 5
print x # prints 5
with changed_value(x, 10):
print x # prints 10
print x # prints 5

This is similar to PEP 343's example 5 ("Redirect stdout temporarily";
see http://www.python.org/dev/peps/pep-0343/), except that the
variable that I want to temporarily change isn't hard-coded in the
stdout_redirected function.

What I want to write is something like this:
@contextmanager
def changed_value(variable, temp_value):
old_value = variable
variable = temp_value
try:
yield None
finally:
variable = old_value
with maybe a check in 'finally' to make sure that the value hasn't
changed during the execution of the 'with'.

Of course this doesn't work since it only changes the binding of
'variable', not whatever was passed in, and I kind of doubt I can
stick a "&" and "*" in a couple places to make it do what I want. :)

So my question is: is what I want possible to do in Python? How?

I think it might be possible to rig something up by passing in the
variable that i want to change as a *string* and looking up that
string in a dictionary somewhere, but I think what I need is the locals
() dictionary of the calling function, and I'm not sure how to get
that.

Thanks,
Evan Driscoll
 
E

Emile van Sebille

On 8/25/2009 12:33 PM Evan Driscoll said...
So my question is: is what I want possible to do in Python?

Probably not with immutables (as in your example) -- maybe otherwise.

Emile
 
E

Evan Driscoll

I want to make a context manager that will temporarily change the
value of a variable within the scope of a 'with' that uses it. This is
inspired by a C++ RAII object I've used in a few projects. Ideally,
what I want is something like the following:

Okay, so I think I actually got this with some consultation with a
friend. Critiques?

from contextlib import contextmanager
import inspect

def get_parents_var(offset, var):
f = inspect.stack()[offset][0]
if var in f.f_locals:
return f.f_locals[var]
else:
return f.f_globals[var]

def set_parents_var(offset, var, val):
f = inspect.stack()[offset][0]
if var in f.f_locals:
f.f_locals[var] = val
elif var in f_globals:
f.f_globals[var] = val
else:
assert False


@contextmanager
def changed_value_tb(var_name, temp_value):
# 1 is here, 2 will be the implicit next() function, 3 is the
real caller
offset = 3
old_value = get_parents_var(offset, var_name)
set_parents_var(offset, var_name, temp_value)
try:
yield None
finally:
set_parents_var(offset, var_name, old_value)


x = 5
print x # prints 5
with changed_value_tb("x", 10):
print x # prints 10
print x # prints 5
 
D

Diez B. Roggisch

Evan said:
(If you don't want to read the following, note that you can answer my
question by writing a swap function.)

I want to make a context manager that will temporarily change the
value of a variable within the scope of a 'with' that uses it. This is
inspired by a C++ RAII object I've used in a few projects. Ideally,
what I want is something like the following:

x = 5
print x # prints 5
with changed_value(x, 10):
print x # prints 10
print x # prints 5

This is similar to PEP 343's example 5 ("Redirect stdout temporarily";
see http://www.python.org/dev/peps/pep-0343/), except that the
variable that I want to temporarily change isn't hard-coded in the
stdout_redirected function.

What I want to write is something like this:
@contextmanager
def changed_value(variable, temp_value):
old_value = variable
variable = temp_value
try:
yield None
finally:
variable = old_value
with maybe a check in 'finally' to make sure that the value hasn't
changed during the execution of the 'with'.

Of course this doesn't work since it only changes the binding of
'variable', not whatever was passed in, and I kind of doubt I can
stick a "&" and "*" in a couple places to make it do what I want. :)

So my question is: is what I want possible to do in Python? How?

No. And unfortunately, the with-statement (as the for-statement and
others) leaks it's name, so you can't even use it like this:


with changed_value(variable, temp_value) as new_name:
...

The new_name will be defined afterwards.

Diez
 
E

Evan Driscoll

Okay, so I think I actually got this with some consultation with a
friend. Critiques?

This is wrong; it's not quite working right. It does with the example
I posted, but not in a more thorough test.

I'm still ignoring the "you can't do this" answers for a little while
more and continuing to try to hack on it. :)

Evan
 
D

Diez B. Roggisch

Evan said:
Okay, so I think I actually got this with some consultation with a
friend. Critiques?

Modifying locals isn't really allowed - it might stop working with
certain implementations of python.

And to be honest - I don't really see a use-case for your whole
approache. Why don't you want to introduce a new name?


Diez
 
E

Evan Driscoll

Modifying locals isn't really allowed - it might stop working with
certain implementations of python.

And to be honest - I don't really see a use-case for your whole
approache. Why don't you want to introduce a new name?

Wow, this actually was very helpful, because it forced me to clarify
what I'm doing to myself, and I *am* making it rather too complicated.

The reason that I can't introduce a new name is that I want to call
another function that refers to the old name. (Specifically, I'm using
this in a testing framework, and I want to mock out some things like
file I/O so want to replace 'open' and others temporarily. I don't
really want to make said function take an extra parameter which is
"the open function to use" or something like that.)

But then I don't need to modify locals. In fact, the references that I
want to affect are used in a function that isn't even on the stack
when the "with" statement is entered!

So I only need to modify a global, which is much easier than what I
was mucking about with.

So here is my simplified version that only works for globals:
from contextlib import contextmanager

@contextmanager
def changed_value(var_name, temp_value):
old_value = globals()[var_name]
globals()[var_name] = temp_value
try:
yield None
finally:
globals()[var_name] = old_value


x = 5
print "5:", x
with changed_value("x", 10):
print "10:", x
print "5:", x

y = 7

def test():
print "5:", x
with changed_value("x", 20):
print "20:", x
print "7:", y
print "5:", x

test()

print "5:", x

How does that look?

And thanks for making me think about what I'm doing. :)

Evan
 
E

Evan Driscoll

So here is my simplified version that only works for globals:

So I think this works if (1) you only use changed_value in the same
module as it's defined in (otherwise it picks up the globals from the
module it's defined in, which is almost certainly not what you want),
and (2) the value you want to change is just a simple variable and not
either something like "os.path" or a builtin.

I have worked on this a bit more and have something that addresses
these issues in at least the cases I've tested. I'm not going to post
the code here, but it is up at
http://pages.cs.wisc.edu/~driscoll/python/utils.py
and there are a few unit tests at
http://pages.cs.wisc.edu/~driscoll/python/utils_test.py

I solved issue (1) by reintroducing the use of inspect to walk back up
the stack a couple frames, but I pulled out the f_globals member
instead of f_locals.

Issue (2a) I fixed by splitting the variable name at periods and
walking through successive dictionaries with each component. Issue
(2b) I fixed by looking for a '__builtins__' entry if the name I'm
looking up doesn't exist.

Right now it's sort of hackish... it probably doesn't respond
particularly well if things go unexpectedly (e.g. a bad variable name
is given) and I should probably verify that the value is unchanged
during the with statement and throw an exception otherwise, but it
probably works well enough for my purposes for now.

Comments are appreciated... a couple of the things in there it seems
like there could very well be reimplementations of things that are
already done.

Evan
 
D

Diez B. Roggisch

Evan said:
So I think this works if (1) you only use changed_value in the same
module as it's defined in (otherwise it picks up the globals from the
module it's defined in, which is almost certainly not what you want),
and (2) the value you want to change is just a simple variable and not
either something like "os.path" or a builtin.

I have worked on this a bit more and have something that addresses
these issues in at least the cases I've tested. I'm not going to post
the code here, but it is up at
http://pages.cs.wisc.edu/~driscoll/python/utils.py
and there are a few unit tests at
http://pages.cs.wisc.edu/~driscoll/python/utils_test.py

I solved issue (1) by reintroducing the use of inspect to walk back up
the stack a couple frames, but I pulled out the f_globals member
instead of f_locals.

Issue (2a) I fixed by splitting the variable name at periods and
walking through successive dictionaries with each component. Issue
(2b) I fixed by looking for a '__builtins__' entry if the name I'm
looking up doesn't exist.

Right now it's sort of hackish... it probably doesn't respond
particularly well if things go unexpectedly (e.g. a bad variable name
is given) and I should probably verify that the value is unchanged
during the with statement and throw an exception otherwise, but it
probably works well enough for my purposes for now.

Comments are appreciated... a couple of the things in there it seems
like there could very well be reimplementations of things that are
already done.


I'd still won't use it :) instead, something like this might be
something I'd use, if I need a local "rebound". Or, again, just use a
different *name* alltogether.

foo = "bar"


@apply
def f(foo="baz"):
...



Other than that, for your original use-case, I have a context-manager I
call "safe modifier" that

- takes an object, key and value
- stores the old value of the key on the object
- sets the new value
- on exit, restores the old value

This is for e.g. temporary config-changes in tests.

Diez
 
C

Carl Banks

I want to make a context manager that will temporarily change the
value of a variable within the scope of a 'with' that uses it. This is
inspired by a C++ RAII object I've used in a few projects. Ideally,
what I want is something like the following:

Okay, so I think I actually got this with some consultation with a
friend. Critiques?

    from contextlib import contextmanager
    import inspect

    def get_parents_var(offset, var):
        f = inspect.stack()[offset][0]
        if var in f.f_locals:
                return f.f_locals[var]
        else:
                return f.f_globals[var]

    def set_parents_var(offset, var, val):
        f = inspect.stack()[offset][0]
        if var in f.f_locals:
            f.f_locals[var] = val
        elif var in f_globals:
            f.f_globals[var] = val
        else:
            assert False

    @contextmanager
    def changed_value_tb(var_name, temp_value):
        # 1 is here, 2 will be the implicit next() function, 3 is the
real caller
        offset = 3
        old_value = get_parents_var(offset, var_name)
        set_parents_var(offset, var_name, temp_value)
        try:
            yield None
        finally:
            set_parents_var(offset, var_name, old_value)

    x = 5
    print x   # prints 5
    with changed_value_tb("x", 10):
        print x  # prints 10
    print x  # prints 5

Well, it wouldn't be a "can I rebind a variable using a with-
statement" thread if someone didn't post a solution that they thought
worked, but didn't test it on local variables.


Carl Banks
 
E

Evan Driscoll

Well, it wouldn't be a "can I rebind a variable using a with-
statement" thread if someone didn't post a solution that they thought
worked, but didn't test it on local variables.

I'm not going to deny it was pretty stupid... though in my defense,
I'm new at more that trivial Python coding and don't have the Python
scoping rules and such ingrained in my mind yet. :)

Evan
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,968
Messages
2,570,153
Members
46,699
Latest member
AnneRosen

Latest Threads

Top