In studying this answer, I discoved, much to my surprise, that exec
has a strange behavior;
>>> def f1():
... return x
...
>>> def f2():
... exec ""
... return x
...
>>> f1()
Traceback (most recent call last):
...
NameError: global name 'x' is not defined
>>> f2()
Traceback (most recent call last):
...
NameError: name 'x' is not defined
>>> x = 'bar'
>>> f1()
'bar'
>>> f2()
'bar'
Obviously, both return some global value x
; but not so if f2()
is changed slightly:
>>> def f2():
... exec "x = 'im local now'"
... return x
...
>>> f2()
'im local now'
f2 returns its own special copy of x
, even though there's nothing in the body of f2 that would seem to cause that (no assignment to x).
I can easy see how this is happening, the presence of the exec
statement changes the LOAD_GLOBAL
bytecode into a LOAD_NAME
, in a similar vein as the presence of yield
turns a function into a generator.
>>> dis.dis(f1)
2 0 LOAD_GLOBAL 0 (x)
3 RETURN_VALUE
>>> dis.dis(f2)
2 0 LOAD_CONST 1 ('')
3 LOAD_CONST 0 (None)
6 DUP_TOP
7 EXEC_STMT
3 8 LOAD_NAME 0 (x)
11 RETURN_VALUE
But what I don't understand is why. Is this documented behavior? is this an implementation detail of cpython? (it works in the same way in IronPython, although the dis
module is non functional)