exec() bytecode with arbitrary locals?

bytecode, codeblocks, python, python-3.x

Solution

You can pass bytecode instead of a string to `exec`, you just need to make the right bytecode for the purpose:

>>> bytecode = compile('value += 5', '<string>', 'exec')
>>> mydict = {'value': 23}
>>> exec(bytecode, mydict)
>>> mydict['value']
28

Specifically, ...:

>>> import dis
>>> dis.dis(bytecode)
  1           0 LOAD_NAME                0 (value)
              3 LOAD_CONST               0 (5)
              6 INPLACE_ADD         
              7 STORE_NAME               0 (value)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE        

the load and store instructions must be of the _NAME persuasion, and this `compile` makes them so, while...:

>>> def f(): value += 5
... 
>>> dis.dis(f.func_code)
  1           0 LOAD_FAST                0 (value)
              3 LOAD_CONST               1 (5)
              6 INPLACE_ADD         
              7 STORE_FAST               0 (value)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

...code in a function is optimized to use the _FAST versions, and those don't work on a dict passed to `exec`. If you started somehow with a bytecode using the _FAST instructions, you could patch it to use the _NAME kind instead, e.g. with bytecodehacks or some similar approach.

Problem

Suppose I want to execute code, for example ``` value += 5 ``` inside a namespace of my own (so the result is essentially `mydict['value'] += 5`). There's a function `exec()`, but I have to pass a string there: ``` exec('value += 5', mydict) ``` and passing statements as strings seems strange (e.g. it's not colorized that way). Can it be done like: ``` def block(): value += 5 ???(block, mydict) ``` ? The obvious candidate for last line was `exec(block.__code__, mydict)`, but no luck: it raises `UnboundLocalError` about `value`. I believe it basically executes `block()`, not the code inside block, so assignments aren't easy – is that correct? Of course, another possible solution would be to disassembly `block.__code__`... FYI, I got the question because of this thread. Also, this is why some (me undecided) call for new syntax ``` using mydict: value += 5 ``` Note how this doesn't throw error but doesn't change `mydict` either: ``` def block(value = 0): value += 5 block(**mydict) ```

Original source