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) ```