To sum it up:
- There is no bug in Python 2 nor in Python 3
- The different behavior of
exec
stems from exec
being a statement in Python 2, while it became a function in Python 3.
Please note:
I do not tell anything new here. This is just an assembly of the truth
out there found in all the other answers and comments.
All I try here is to bring light to some of the more obscure details.
The only difference between Python 2 and Python 3 is, that, indeed, exec
is able to change the local scope of the enclosing function in Python 2 (because it is a statement and can access the current local scope) and cannot do this anymore in Python 3 (because it now is a function, so runs in it's own local scope).
The irritation, however, has nothing to do with the exec
statement, it only stems from one special behavior detail:
locals()
returns something, which I want to call "a scope-wise mutable singleton which, after the call to locals()
, always only references all variables in the local scope".
Please note that the behavior of locals()
did not change between Python 2 and 3. So, this behavior together with change of how exec
works looks like being erratic, but isn't, as it just exposes some detail, which always was there.
What does "a scope-wise mutable singleton which references variables in local scope" mean?
- It is a
scope-wise singleton
, as regardless how often you call locals()
in the same scope, the object returned is always the same.
- Hence the observation, that
id(d) == id(locals())
, because d
and locals()
refer to the same object, the same singleton, as there can only be one (in a different scope you get a different object, but in the same scope you only see this single one).
- It is
mutable
, as it is a normal object, so you can alter it.
locals()
forces all entries in the object to reference the variables in the local scope again.
- If you change something in the object (via
d
), this alters the object, as it is a normal mutable object.
These changes of the singleton do not propagate back into the local scope, because all entries in the object are references to the variables in the local scope
. So if you alter entries, these changes the singleton object, and not the contents of where "the references pointed to before you change the reference" (hence you do not alter the local variable).
In Python, Strings and Numbers are not mutable. This means, if you assign something to an entry, you do not change the object where the entry points to, you introduce a new object and assign a reference to that to the entry. Example:
a = 1
d = locals()
d['a'] = 300
# d['a']==300
locals()
# d['a']==1
Besides optimization this does:
- Create new object Number(1) - which is some other singleton, BTW.
- store pointer to this Number(1) into
LOCALS['a']
(where LOCALS
shall be the internal local scope)
- If not already exist, create
SINGLETON
object
- update
SINGLETON
, so it references all entries in LOCALS
- store pointer of the
SINGLETON
into LOCALS['d']
- Create Number(300), which is not a singleton, BTW.
- store pointer to these Number(300) into
d['a']
- hence the
SINGLETON
is updated, too.
- but
LOCALS
is not updated,
so the local variable a
or LOCALS['a']
still is Number(1)
- Now,
locals()
is called again, the SINGLETON
is updated.
- As
d
refers to SINGLETON
, not LOCALS
, d
changes, too!
For more on this surprising detail, why 1
is a singleton while 300
is not, see https://stackoverflow.com/a/306353
But please do not forget: Numbers are immutable, so if you try to change a number to another value, you effectively create another object.
Conclusion:
You cannot bring back the exec
behavior of Python 2 to Python 3 (except by changing your code), as there is no way to alter the local variables outside of the program flow anymore.
However, you can bring the behavior of Python 3 to Python 2, such that you, today, can write programs, which run the same, regardless if they run with Python 3 or Python 2. This is because in (newer) Python 2 you can use exec
with function like arguments as well (in fact, those is a 2- or 3-tuple), with allows to use the same syntax with the same semantics known from Python 3:
exec "code"
(which only works in Python 2) becomes (which works for Python 2 and 3):
exec("code", globals(), locals())
But beware, that "code"
can no more alter the local enclosing scope this way. See also https://docs.python.org/2/reference/simple_stmts.html#exec
Some very last words:
The change of exec
in Python 3 is good. Because of optimization.
In Python 2 you were not able to optimize across exec
, because the state of all local variables which contained immutable contents could change unpredictably. This cannot happen anymore. Now the usual rules of function invocations apply to exec()
like to all other functions, too.