如何在使用`exec`调用时更新局部变量?

56 投票
3 回答
26137 浏览
提问于 2025-04-15 14:31

我原以为这段代码会输出3,但实际上它输出的是1:

# Python3

def f():
    a = 1
    exec("a = 3")
    print(a)

f()
# 1 Expected 3

3 个回答

4

你不能通过 exec 来改变函数里的局部变量,原因可以简单总结如下:

  1. exec 是一个函数,它的局部作用域和它被调用时最内层的作用域是共享的。
  2. 每当你在一个函数的作用域内定义一个新对象时,它会在这个函数的局部命名空间中可用,也就是说,它会修改 local() 字典。当你在 exec 中定义一个新对象时,它的效果大致相当于下面的内容:

from copy import copy
class exec_type:
    def __init__(self, *args, **kwargs):
        # default initializations
        # ...
        self.temp = copy(locals())

    def __setitem__(self, key, value):
        if var not in locals():
            set_local(key, value)
        self.temp[key] = value

temp 是一个临时的命名空间,每次调用 exec 时都会重置。


  1. Python 查找名字的顺序是从局部命名空间开始的,这个过程被称为 LEGB 规则。Python 会先查找局部命名空间,然后是外层作用域,再接着是全局命名空间,最后是内置命名空间。

一个更全面的例子可能是这样的:

g_var = 5

def test():
    l_var = 10
    print(locals())
    exec("print(locals())")
    exec("g_var = 222")
    exec("l_var = 111")
    exec("print(locals())")

    exec("l_var = 111; print(locals())")

    exec("print(locals())")
    print(locals())
    def inner():
        exec("print(locals())")
        exec("inner_var = 100")
        exec("print(locals())")
        exec("print([i for i in globals() if '__' not in i])")

    print("Inner function: ")
    inner()
    print("-------" * 3)
    return (g_var, l_var)

print(test())
exec("print(g_var)")

输出:

{'l_var': 10}
{'l_var': 10}

局部变量是相同的。

{'l_var': 10, 'g_var': 222}

在添加了 g_var 并改变 l_var 后,只添加了 g_var,而 l_var 保持不变。

{'l_var': 111, 'g_var': 222}

l_var 被改变是因为我们在一次调用 exec 时同时改变和打印了局部变量。

{'l_var': 10, 'g_var': 222}
{'l_var': 10, 'g_var': 222}

在函数的局部变量和 exec 的局部变量中,l_var 都没有改变,而 g_var 被添加了。

Inner function: 
{}
{'inner_var': 100}
{'inner_var': 100}

inner_function 的局部变量和 exec 的局部变量是相同的。

['g_var', 'test']

全局命名空间只包含 g_var 和函数名(排除特殊方法后)。

---------------------

(5, 10)
5
5

如果你在一个方法里面,你可以这样做:

# python 2 or 3
class Thing():
    def __init__(self):
        exec('self.foo = 2')
    
x = Thing()
print(x.foo)

你可以在这里了解更多信息

84

这个问题在Python3的错误列表中有一些讨论。要实现这种行为,你需要这样做:

def foo():
    ldict = {}
    exec("a=3",globals(),ldict)
    a = ldict['a']
    print(a)

如果你查看Python3关于exec的文档,你会看到以下说明:

默认的局部变量就像下面的locals()函数描述的那样:不应该尝试修改默认的局部变量字典。如果你需要在函数执行后查看代码对局部变量的影响,请传递一个明确的局部变量字典。

这意味着,单参数的exec不能安全地执行任何会绑定局部变量的操作,包括变量赋值、导入、函数定义、类定义等等。如果使用global声明,它可以对全局变量进行赋值,但不能对局部变量。

回到这个错误报告中的具体信息,Georg Brandl说:

在不产生多个后果的情况下,动态修改函数的局部变量是不可能的:通常,函数的局部变量并不是存储在字典中,而是存储在一个数组中,这个数组的索引是在编译时根据已知的局部变量确定的。这与exec添加的新局部变量发生冲突。旧的exec语句绕过了这个问题,因为编译器知道如果在函数中出现没有全局/局部参数的exec,那么那个命名空间将是“未优化”的,也就是说不使用局部变量数组。由于exec()现在是一个普通函数,编译器不知道“exec”可能绑定到什么,因此无法特别处理它

强调是我的。

所以,总的来说,Python3通过默认不允许这种行为,能够更好地优化局部变量的使用。

为了完整起见,正如上面评论中提到的,这在Python 2.X中确实按预期工作:

Python 2.6.2 (release26-maint, Apr 19 2009, 01:56:41) 
[GCC 4.3.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def f():
...     a = 1
...     exec "a=3"
...     print a
... 
>>> f()
3

撰写回答