I thought this would print 3, but it prints 1:
def f():
a = 1
exec(\"a = 3\")
print(a)
If you are inside a method, you can do so:
class Thing():
def __init__(self):
exec('self.foo = 2')
x = Thing()
print(x.foo)
You can read more about it here
The reason that you can't change local variables within a function using exec
in that way, and why exec
acts the way it does, can be summarized as following:
exec
is a function that shares its local scope with the scope of the most inner scope in which it's called. local()
dictionary. When you define a new object in exec
what it does is roughly equivalent to following:from copy import copy
class exec_type:
def __init__(self, *args, **kwargs):
# default initializations
# ...
self.temp = copy(locals())
def __setitem__(self, key, value):
if var not in locals():
set_local(key, value)
self.temp[key] = value
temp
is a temporary namespace that resets after each instantiation (each time you call the exec
).
A more comprehensive example would be something like following:
g_var = 5
def test():
l_var = 10
print(locals())
exec("print(locals())")
exec("g_var = 222")
exec("l_var = 111")
exec("print(locals())")
exec("l_var = 111; print(locals())")
exec("print(locals())")
print(locals())
def inner():
exec("print(locals())")
exec("inner_var = 100")
exec("print(locals())")
exec("print([i for i in globals() if '__' not in i])")
print("Inner function: ")
inner()
print("-------" * 3)
return (g_var, l_var)
print(test())
exec("print(g_var)")
Output:
{'l_var': 10}
{'l_var': 10}
locals are the same.
{'l_var': 10, 'g_var': 222}
after adding g_var
and changing the l_var
it only adds g_var
and left the l_var
unchanged.
{'l_var': 111, 'g_var': 222}
l_var
is changed because we are changing and printing the locals in one instantiation ( one call to exec).
{'l_var': 10, 'g_var': 222}
{'l_var': 10, 'g_var': 222}
In both function's locals and exec's local l_var
is unchanged and g_var
is added.
Inner function:
{}
{'inner_var': 100}
{'inner_var': 100}
inner_function
's local is same as exec's local.
['g_var', 'test']
global is only contain g_var
and function name (after excluding the special methods).
---------------------
(5, 10)
5
This issue is somewhat discussed in the Python3 bug list. Ultimately, to get this behavior, you need to do:
def foo():
ldict = {}
exec("a=3",globals(),ldict)
a = ldict['a']
print(a)
And if you check the Python3 documentation on exec, you'll see the following note:
The default locals act as described for function
locals()
below: modifications to the default locals dictionary should not be attempted. Pass an explicit locals dictionary if you need to see effects of the code on locals after function exec() returns.
That means that one-argument exec
can't safely perform any operations that would bind local variables, including variable assignment, imports, function definitions, class definitions, etc. It can assign to globals if it uses a global
declaration, but not locals.
Referring back to a specific message on the bug report, Georg Brandl says:
To modify the locals of a function on the fly is not possible without several consequences: normally, function locals are not stored in a dictionary, but an array, whose indices are determined at compile time from the known locales. This collides at least with new locals added by exec. The old exec statement circumvented this, because the compiler knew that if an exec without globals/locals args occurred in a function, that namespace would be "unoptimized", i.e. not using the locals array. Since exec() is now a normal function, the compiler does not know what "exec" may be bound to, and therefore can not treat is specially.
Emphasis is mine.
So the gist of it is that Python3 can better optimize the use of local variables by not allowing this behavior by default.
And for the sake of completeness, as mentioned in the comments above, this does work as expected in Python 2.X:
Python 2.6.2 (release26-maint, Apr 19 2009, 01:56:41)
[GCC 4.3.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def f():
... a = 1
... exec "a=3"
... print a
...
>>> f()
3