2

I'm using an exec() statement to set a value, like so:

foo = 3
def return_4():
    return 4
instruction = 'foo = return_4()'
exec(instruction)                     # <---- WHERE THE MAGIC HAPPENS
print(foo)

This comes out as 4, as I expect.

My program has operations for manipulating a Rubik's cube. In this stripped down version, I'll do four things:

  1. I'll instantiate a cube, filling in one face (with abbreviations for 'front top left' and 'front bottom right' and the like).

  2. I'll have a function that rotates that front face.

  3. I'll have an 'interpreter' function, which takes a cube and a list of instructions, and applies those instructions to the cube, returning the modified cube. Here's where I use 'exec' (and where I think the breakage happens).

  4. Finally, I'll run the interpreter on my partial cube with the instruction to rotate the face one time.

+

my_cube = [['FTL', 'FTM', 'FTR',
            'FML', 'FMM', 'FMR',
            'FBL', 'FBM', 'FBR'],
            [],[],[],[],[]] # other faces specified in actual code

def rotate_front(cube):
    front = cube[0]
    new_front = [front[6],front[3],front[0],
                 front[7],front[4],front[1],
                 front[8],front[5],front[2]]
    # ...
    ret_cube = cube
    ret_cube[0] = new_front
    # pdb says we are returning a correctly rotated cube,
    # and calling this directly returns the rotated cube
    return ret_cube

def process_algorithm(cube=default_cube, algorithm=[]):
    return_cube = cube
    for instruction in algorithm:
        exec('return_cube = ' + instruction + '(return_cube)')  # <--- NO MAGIC!
        # ACCORDING TO pdb, return_cube HAS NOT BEEN ROTATED!
    return return_cube

process_algorithm(cube = my_cube, algorithm = ['rotate_front'])

If I replace the exec(x = y) formation with x = eval(y), it seems to work. return_cube = eval(instruction + '(return_cube)')

So maybe this is just academic. Why does the toy example work, and the actual code fail? (Am I doing something obvious and silly, like missing an equals sign? I'm going to kick myself, I bet...)

Thanks for any help anyone can offer.

Brownbat
  • 266
  • 1
  • 8
  • 2
    `exec` is not a statement anymore. So it cannot change variable lookup. Do you really need it? – JBernardo Jan 02 '12 at 00:47
  • Have you tried debugging by printing out `'return_cube = ' + instruction + '(return_cube)'` ? – Ilya Kogan Jan 02 '12 at 00:48
  • 1
    Thanks for the replies. JBernardo: I'm sorry, I'm not sure what you mean by "change variable lookup." Keep in mind it works in the toy example somehow. No, I don't need it, eval() now works great, this has become academic. Ilya: printing that provides the correct and expected instruction. Running that exact instruction outside of exec() works perfectly somehow. Hardcoding that exact instruction into the exec() still fails. Puzzling, eh? – Brownbat Jan 02 '12 at 00:52
  • 2
    Although I realize it's not the point of your question, I would implement this not by passing the names of the steps in the algorithm, but by passing the actual functions. – David Z Jan 02 '12 at 00:56
  • @Brownbat Check my answer. The "toy code" works because it is already dealing with global code. – JBernardo Jan 02 '12 at 01:02

2 Answers2

5

On Python 2.x, exec was a statement that changed variable lookup from LOAD_GLOBAL and LOAD_FAST to LOAD_NAME to every name you access on your function. That means it first search the local scope to see if can find the name to after check the global scope.

Now, on Python 3.x, the exec function cannot change this lookup and will never find the name you defined unless you add an argument with the scope you want the result to be evaluated.

exec(some_code, globals())

For that to work, you need to add global my_var inside the function to make sure the lookup will work.

Keep in mind these things will be inserted in the global namespace of your module...

BTW, why do you need exec or eval? Why can't you add real functions to your algorithm list?


As side note, I can see you don't change the algorithm var on your function, but if you do it'll introduce some undesired side effects because the default value you created is mutable and will be used on all function calls.

For safety, change it to None and create a new list if needed.

JBernardo
  • 32,262
  • 10
  • 90
  • 115
  • Sure enough, I can break the toy example by putting it all inside a function. I'm not sure I need exec. Adding the functions directly seems smart. I guess with eval/exec I could pull instructions from human readable text files, that's kinda nice... probably not nice enough though. Sending the functions through directly is the new plan... Thanks for the help! – Brownbat Jan 02 '12 at 01:05
  • @Brownbat Yon can have a mapping of function names: `funcs = {'x': func_x, 'y': func_y}`. – JBernardo Jan 02 '12 at 01:20
3

This doesn't attempt to answer the question, but rather expands the test-cases so the behavior can be seen better and preserved for reference. The results come from Python 3.2.2 on Windows. See JBernardo's answer for a "why" this behavior occurs.

From global scope ("toy example"):

>>> foo = "global-foo"
>>> exec('foo = "global-bar"')
>>> foo
'global-bar'

In a function (full context):

>>> def setIt():
        foo = "local-foo"
        exec('foo = "local-bar"')
        return foo

foo = "global-foo"
>>> setIt()
'local-foo'
>>> foo
'global-foo'

With specifying globals() (does not work for locals() at all):

>>> def setIt():
        foo = "local-foo"
        exec('foo = "local-bar"', globals())
        return foo

>>> foo = "global-foo"
>>> setIt()
'local-foo'
>>> foo
'local-bar'

Happy coding.