In an update in yesterday's entry on scopes and what bytecodes they use I said that the special *_NAME opcodes could wind up being used in functions but that it would take an entire entry to explain when and why, and I also included a trivia bonus that I need to explain. It will probably not surprise you to know that these two things turn out to be intimately connected.
To start with, here is an altered version of yesterday's bonus trivia that adds a second oddity:
def geta():
return a
def exec_and_return(codeobj, x):
exec codeobj
return x, geta(), a
a = 5
co = compile("a = a + x; x = a", '<s>', 'exec')
print exec_and_return(co, 4)
print a
This prints '
(9, 5, 9)
' and then '
5
'.
Wait, what?
Before I start trying to explain this, let's talk about all of the things that are wrong with this result. First off, if the
compile()
code was simply inline in place of the
exec
we would get an UnboundLocalError since
a
is used before it's assigned to. Second, if the
a
was instead being treated as just a plain global its global value should change and we can see that it doesn't, either while the function is running or after it finishes; at the same time, the global value of
a
was clearly used to compute the result. Finally, something that looks a lot like a new local
a
is visible in the function with the value that we expect (the same value as
x
).
(You can also verify that
a
appears in the dictionary returned by
locals()
.)
The first thing going on here is what happens inside the compiled code as
exec
runs it. Recall that NAME opcodes essentially treat a variable as global until it is assigned to, at which point it becomes a local, and that
compile()
generates NAME opcodes. This means that in the compiled
co
code object, the value of
a
is first read from the global
a
but then the assignment creates a local
a
(in the stack frame that
exec
uses to run the code); it is this local
a
that is then assigned to
x
. This explains why the global
a
never changes value; it is never written to, despite appearances. What the compiled code really means is something like '
al = a + x; x = al
'.
(This is the entire explanation for yesterday's trivia contest.)
The second thing going on is that CPython tries quite hard to let
exec
'd code create new local variables in functions . It does this by changing what bytecodes get generated for references to variables that aren't definitely locals. Under normal circumstances CPython decides that you clearly mean a global variable and compiles to bytecodes that use the *_GLOBAL family of opcodes to access things. However, if you use
exec
in your function CPython decides that such references are unclear and compiles them to *_NAME opcodes instead, since you could also be trying to access new local variables that will be created inside the code run by
exec
.
(Since NAME opcodes look first at locals and then secondly at globals, this will still work if you are genuinely referring to a global variable. The example contains an instance of this in our call to
geta()
, as you can see by disassembling the bytecode for
exec_and_return
.)
But just compiling such references to NAME opcodes isn't enough enough to do the job by itself. Because NAME opcodes explicitly look at the frame's
f_locals
dictionary, you need a real dictionary and it really holds (some) local variables in it. This means that a function that uses
exec
effectively has two sets of local variables ; it has the known fast local variables , stored in the local variable array and accessed with FAST opcodes, and then also any additional variables that were materialized by
exec
'd code, stored in the frame locals dictionary and accessed with NAME opcodes (if they're accessed at all).
(Since I was just checking this: while
exec
does wind up creating a new frame to run the compiled code in, as far as I can tell it does not create a new frame locals dictionary to go with it. Instead it directly reuses the frame locals dictionary of the current frame. The fast local variables are synchronized into the frame locals dictionary before it's used and
exec
imperfectly copies changes to them back to the local variables array after the code finishes running.)
Frankly, all of this is confusing and arcane and just goes to show how much of an impact on your language there is to try to support executing arbitrary code in the scope of a function (at least in the face of various sorts of important optimizations). We are up to one bug, conditional bytecode generation with unusual semantics, and several pieces of odd weirdness.