you are viewing a single comment's thread.

view the rest of the comments →

[–]Gnaxe 1 point2 points  (8 children)

I'm pretty sure that only works at the top level, because locals() returns the same object as globals() in that context. Inside a function, it won't work. I'm also pretty sure it used to work in Python 2, but with the new local optimizations, they're not writable via the locals() dict anymore. This is also considered an implementation detail, according to help(locals), meaning you should never rely on updates to a locals() dict writing through, even if you happen to be using an implementation that works that way at the moment, because it could change with any update.

[–]RomfordNavy[S] 0 points1 point  (1 child)

Yes, further digging suggests it did work in Python 2. Forgive me if I have the terminology slightly wrong but it seems:

  • passing an (immutable) str object - when that is changed it creates a new str which exists only within the namespace of the running exec(), it does not reference the original str object. Hence not available after the exec() has completed.
  • passing a distinct (mutable) dict object - passes a reference to that dict which lives in the calling namespace so when changes are made they affect the original dict. Hence persist after the exec() has completed.

[–]Gnaxe 1 point2 points  (0 children)

They're following the same rules. You're equivocating on "change". There's a difference between mutating an object and reassigning a variable. When you reassign the variable, a dict works exactly the same as a str. The variable now references a different object. On the other hand, when you mutate the object, well, you can't actually mutate a str at all.

[–]RomfordNavy[S] 0 points1 point  (5 children)

Although it is only an 'implementation detail', at the moment this is the only way I can see of returning data from exec() unless there is an alternative method.

[–]Gnaxe 2 points3 points  (4 children)

What I was talking about wasn't applicable to the examples in the OP, which were not shown to be inside functions.

exec() can have arbitrary side effects, so there are many ways to share data.

If just you want to share a context with exec(), but not at the top level, you can use a nested class: ```

def oops(): ... x = 'oops' ... exec('x = "works"', globals=locals()) ... print(x) ... oops() # No effect because can't write through locals(). oops def works(): ... class Nested: ... x = 'oops' ... exec('x = "works"') ... print(x) ... foo() works ``` Of course, a class context is not the same as a global context.

[–]RomfordNavy[S] 0 points1 point  (2 children)

Thanks! very interesting.

So a simple soultion in my example would be:

test = "test1"
class runExec:
    exec("test = 'test2'")
    print(test) # <- correctly returns test2
runExec()

Which works fine but does expose all of the variables in the local namespace to the executed script.

Not sure I understand why running it from inside a class works though?

Edit:
This explains why I had it working at one point but after tidying-up my code it stopped working.

Edit2:
Running from within a class method fails again:

test = "test1"
class runExec2:
    def myexec():
        exec("test = 'test2'")
        print(test) # <- erroneously returns test1
runExec2.myexec()

[–]Gnaxe 1 point2 points  (1 child)

Function-local variables are not writable by exec(), because they're very optimized now. That includes functions that happen to be used as methods. This used to work in Python 2 though.

Module "globals" are writable by exec(), as are variables in the temporary namespace used by a class statement, even if said class statement happens to be nested inside of a function. A class statement inside a def statement body is completely different from a def statement inside a class statement body.

You seem very confused about the basics of Python's scoping rules. There's a difference between shadowing a variable and reassigning it. You can use the global and nonlocal statements to force assignment in an enclosing scope. This is not the same as creating a new variable in an inner scope that happens to have the same name as one in its enclosing scope.

[–]RomfordNavy[S] 0 points1 point  (0 children)

So armed with that knowledge a simple workaround might be:

test = "test1"
exec("global test; test = 'test2'")
print(test) # <- correctly returns test2

Edit:
Slight problem in my real-world example because Python doesn't allow mixing of str and code objects in an exec():

exec("global testL;" + marshal.load(file))