exec() 字节码与任意局部变量?

3 投票
4 回答
3504 浏览
提问于 2025-04-15 13:36

假设我想执行一些代码,比如:

    value += 5

在我自己定义的命名空间里(这样结果基本上就是 mydict['value'] += 5)。有一个函数叫 exec(),但我必须把代码作为字符串传进去:

    exec('value += 5', mydict) 

把语句作为字符串传进去感觉有点奇怪(比如,这样的话就没有颜色高亮了)。 有没有办法像这样做:

    def block():
        value += 5

    ???(block, mydict)

?最后一行明显可以用 exec(block.__code__, mydict),但没有成功:它抛出了 UnboundLocalError,说 value 没有绑定。我认为这基本上是执行 block(),而不是 块里面的代码,所以赋值操作就不太容易了——这样理解对吗?

当然,另一个可能的解决方案是对 block.__code__ 进行反汇编...

顺便说一下,我之所以有这个问题,是因为看了 这个讨论串。这也是为什么有些人(我还没决定)呼吁需要新的语法。

    using mydict: 
        value += 5

注意,这样做不会抛出错误,但也不会改变 mydict

    def block(value = 0):
        value += 5

    block(**mydict)

4 个回答

3

这里有一个很特别的装饰器,它可以创建一个使用“自定义局部变量”的代码块。实际上,这只是一个快速的解决办法,能让函数内部的所有变量访问变成全局访问,并用自定义的局部变量字典作为环境来计算结果。

import dis
import functools
import types
import string

def withlocals(func):
    """Decorator for executing a block with custom "local" variables.

    The decorated function takes one argument: its scope dictionary.

    >>> @withlocals
    ... def block():
    ...     counter += 1
    ...     luckynumber = 88

    >>> d = {"counter": 1}
    >>> block(d)
    >>> d["counter"]
    2
    >>> d["luckynumber"]
    88
    """
    def opstr(*opnames):
        return "".join([chr(dis.opmap[N]) for N in opnames])

    translation_table = string.maketrans(
            opstr("LOAD_FAST", "STORE_FAST"),
            opstr("LOAD_GLOBAL", "STORE_GLOBAL"))

    c = func.func_code
    newcode = types.CodeType(c.co_argcount,
                             0, # co_nlocals
                             c.co_stacksize,
                             c.co_flags,
                             c.co_code.translate(translation_table),
                             c.co_consts,
                             c.co_varnames, # co_names, name of global vars
                             (), # co_varnames
                             c.co_filename,
                             c.co_name,
                             c.co_firstlineno,
                             c.co_lnotab)

    @functools.wraps(func)
    def wrapper(mylocals):
        return eval(newcode, mylocals)
    return wrapper

if __name__ == '__main__':
    import doctest
    doctest.testmod()

这其实是对某个聪明的人的一个goto装饰器的猴子补丁式改编。

3

使用 global 这个关键词,可以让你在代码块里面修改任何你想要的变量,强制使用动态作用域。

def block():
    global value
    value += 5

mydict = {"value": 42}
exec(block.__code__, mydict)
print(mydict["value"])
7

你可以把字节码传给 exec,而不是字符串,只要你制作出合适的字节码就行:

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

具体来说,...:

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

加载和存储指令必须是 _NAME 类型的,而这个 compile 函数可以把它们变成这样,同时...:

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

...函数中的代码会优化成使用 _FAST 版本,但这些在传给 exec 的字典上是不能用的。如果你一开始用的是 _FAST 指令的字节码,你可以修改它,改成 _NAME 类型的,比如用 bytecodehacks 或者类似的方法。

撰写回答