14

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
Alan Xu
  • 141
  • 5

1 Answers1

1

According to the documentation :

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.

So I believe this falls under "unexpected behavior", but I guess that you can go to the implementation of exec() to really dig in and understand.

zenpoy
  • 19,490
  • 9
  • 60
  • 87