11

I looked through the myriad 'Python exec' threads on SO, but couldn't find one that answered my issue. Terribly sorry if this has been asked before. Here's my problem:

# Python 2.6: prints 'it is working'
# Python 3.1.2: "NameError: global name 'a_func' is not defined"
class Testing(object):
  def __init__(self):
    exec("""def a_func():
      print('it is working')""")
    a_func()

Testing()

# Python 2.6: prints 'it is working'
# Python 3.1.2: prints 'it is working'
class Testing(object):
  def __init__(self):
    def a_func():
      print('it is working')
    a_func()

Testing()

As the standard function definition works in both Python versions, I'm assuming the problem must be a change to the way exec works. I read the API docs for 2.6 and 3 for exec and also read the "What's New In Python 3.0" page and couldn't see any reason why the code would break.

Jedidiah Hurt
  • 812
  • 9
  • 20
  • 5
    That seems like code that is highly likely to make whoever has to maintain it in 5 years cry. – Amber Jul 03 '11 at 06:41
  • 4
    I think this is more of a contained example than actual use of code. I hear `exec` and `eval` have their places in the language. – Ehtesh Choudhury Jul 03 '11 at 06:51

2 Answers2

11

You can see the generated bytecode for each Python version with:

>>> from dis import dis

And, for each interpreter:

#Python 3.2
>>> dis(Testing.__init__)
...
  5          10 LOAD_GLOBAL              1 (a_func)
...

#Python 2.7
>>> dis(Testing.__init__)
...
  5           8 LOAD_NAME                0 (a_func)
...

As you can see, Python 3.2 searches for a global value (LOAD_GLOBAL) named a_func and 2.7 first searches the local scope (LOAD_NAME) before searching the global one.

If you do print(locals()) after the exec, you'll see that a_func is created inside the __init__ function.

I don't really know why it's done that way, but seems to be a change on how symbol tables are processed.

BTW, if want to create a a_func = None on top of your __init__ method to make the interpreter know it's a local variable, it'll not work since the bytecode now will be LOAD_FAST and that don't make a search but directly gets the value from a list.

The only solution I see is to add globals() as second argument to exec, so that will create a_func as a global function an may be accessed by the LOAD_GLOBAL opcode.

Edit

If you remove the exec statement, Python2.7 change the bytecode from LOAD_NAME to LOAD_GLOBAL. So, using exec, your code will always be slower on Python2.x because it has to search the local scope for changes.

As Python3's exec is not a keyword, the interpreter can't be sure if it's really executing new code or doing something else... So the bytecode don't change.

E.g.

>>> exec = len
>>> exec([1,2,3])
3

tl;dr

exec('...', globals()) may solve the problem if you don't care the result being added to global namespace

JBernardo
  • 32,262
  • 10
  • 90
  • 115
  • It worked. Great answer; the bytecode introspection is really cool. Didn't know about that. Thanks for sharing. – Jedidiah Hurt Jul 03 '11 at 07:55
  • 1
    As `exec` is considering the local scope, and as `locals()` stores it, one can also do `exec("a_func()")` or `locals()['a_func']()`. – MGwynne Jul 03 '11 at 08:08
  • 1
    So the basic jist is that in 2.7 python is doing some hackery to notice such things as `exec` is a built-in, but now `exec` is "just another function", such hackery has been removed? – MGwynne Jul 03 '11 at 08:09
  • @MGGwynne: "...such hackery has been removed". I see. I'm fairly new to Python, but I've done some reading over the past couple days and read [the doc](http://docs.python.org/release/3.0.1/whatsnew/3.0.html) covering the philosophy behind Python 3 (i.e. breaking backwards compatibility to remove warts), so this change makes sense. – Jedidiah Hurt Jul 03 '11 at 15:58
6

Completing the answer above, just in case. If the exec is in some function, I would recommend using the three-argument version as follows:

def f():
    d = {}
    exec("def myfunc(): ...", globals(), d)
    d["myfunc"]()

This is the cleanest solution, as it doesn't modify any namespace under your feet. Instead, myfunc is stored in the explicit dictionary d.

Armin Rigo
  • 12,048
  • 37
  • 48