Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

exec() bytecode with arbitrary locals?

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)
like image 569
ilya n. Avatar asked Aug 04 '26 14:08

ilya n.


2 Answers

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.

like image 69
Alex Martelli Avatar answered Aug 06 '26 04:08

Alex Martelli


Use the global keyword to force dynamic scoping on any variables you want to modify from within the block:

def block():
    global value
    value += 5

mydict = {"value": 42}
exec(block.__code__, mydict)
print(mydict["value"])
like image 39
John Millikin Avatar answered Aug 06 '26 03:08

John Millikin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!