The following code snippet works as expected:
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0')
print(f'local symbol table after exec : {locals()}')
test()
# printed result:
# local symbol table before exec : {}
# local symbol table after exec : {'a': 0}
However, once I add a symbol definition statement a = 1
at the end of test
function, it seems that the exec
statement has no effect on the local symbol table:
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0')
print(f'local symbol table after exec : {locals()}')
a = 1
test()
# printed result:
# local symbol table before exec : {}
# local symbol table after exec : {}
So, why is this happening?
Here is my guess: symbols statically defined inside a function will be reserved at compile time in some way, and any symbol definition statements dynamically called inside the exec function wont be able to modify the local symbol table if the symbol is already reserved.
Is that true? What is actually going on during the compile time?
Extra Test 1: replacing the exec argument with 'a = 0\nprint(locals())'
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0\nprint(locals())')
print(f'local symbol table after exec : {locals()}')
test()
# printed result:
# local symbol table before exec : {}
# {'a': 0}
# local symbol table after exec : {'a': 0}
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0\nprint(locals())')
print(f'local symbol table after exec : {locals()}')
a = 1
test()
# printed result:
# local symbol table before exec : {}
# {'a': 0}
# local symbol table after exec : {}
As we can see, the symbol a
was successfully added to the local symbol table during the exec()
execution, but it magically disappeared right after that with the existence of a = 1
.
Extra Test 2: adding return
statement before a = 1
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0\nprint(locals())')
print(f'local symbol table after exec : {locals()}')
return
test()
# printed result:
# local symbol table before exec : {}
# {'a': 0}
# local symbol table after exec : {'a': 0}
def test():
print(f'local symbol table before exec : {locals()}')
exec('a = 0\nprint(locals())')
print(f'local symbol table after exec : {locals()}')
return
a = 1
test()
# printed result:
# local symbol table before exec : {}
# {'a': 0}
# local symbol table after exec : {}
The a = 1
is unreachable in the second test()
function, but it still affects the behaviour of exec()
.
Even the dis()
function from dis
module can't tell the difference between these two test()
functions. The outputs are exactly the same, which is shown below:
5 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('local symbol table before exec : ')
4 LOAD_GLOBAL 1 (locals)
6 CALL_FUNCTION 0
8 FORMAT_VALUE 0
10 BUILD_STRING 2
12 CALL_FUNCTION 1
14 POP_TOP
6 16 LOAD_GLOBAL 2 (exec)
18 LOAD_CONST 2 ('a = 0\nprint(locals())')
20 CALL_FUNCTION 1
22 POP_TOP
7 24 LOAD_GLOBAL 0 (print)
26 LOAD_CONST 3 ('local symbol table after exec : ')
28 LOAD_GLOBAL 1 (locals)
30 CALL_FUNCTION 0
32 FORMAT_VALUE 0
34 BUILD_STRING 2
36 CALL_FUNCTION 1
38 POP_TOP
8 40 LOAD_CONST 0 (None)
42 RETURN_VALUE