如何从另一个模块更改模块变量?

160 投票
5 回答
90026 浏览
提问于 2025-04-16 03:06

假设我有一个叫做 bar 的包,它里面有一个文件 bar.py

a = None

def foobar():
    print a

还有一个 __init__.py 文件:

from bar import a, foobar

然后我执行这个脚本:

import bar

print bar.a
bar.a = 1
print bar.a
bar.foobar()

这是我期待的结果:

None
1
1

这是我实际得到的结果:

None
1
None

有没有人能解释一下我哪里理解错了?

5 个回答

24

换句话说:这个误解其实很容易产生。在Python的语言参考中,有个比较隐晦的定义:使用对象而不是符号。我建议Python的语言参考能更清楚地说明这一点,而不是这么简略。

from的形式并不会绑定模块名称:它会遍历标识符列表,查找每一个标识符在步骤(1)中找到的模块,并将这个名字绑定到本地命名空间中的对象

但是:

当你导入时,你导入的是被导入符号的当前值,并将其添加到你定义的命名空间中。 你并不是在导入一个引用,而是实际上在导入一个值。

因此,要获取i的更新值,你必须导入一个持有该符号引用的变量。

换句话说,导入并不像JAVA中的import,C/C++中的external声明,甚至也不是PERL中的use子句。

相反,Python中的以下语句:

from some_other_module import a as x

更像是K&R C中的以下代码:

extern int a; /* import from the EXTERN file */

int x = a;

(注意:在Python的情况下,“a”和“x”本质上是对实际值的引用:你不是在复制整数,而是在复制引用地址)

33

这个问题的一个难点在于你有一个名为 bar/bar.py 的程序:当你使用 import bar 时,它会根据你在哪里调用,导入 bar/__init__.pybar/bar.py,这让人有点难以追踪哪个 abar.a

下面是它的工作原理:

理解发生了什么的关键是要明白在你的 __init__.py 中,

from bar import a

实际上做的事情类似于

a = bar.a
# … where bar = bar/bar.py (as if bar were imported locally from __init__.py)

并定义了一个新变量(如果你愿意,可以称为 bar/__init__.py:a)。因此,你在 __init__.py 中的 from bar import a 将名称 bar/__init__.py:a 绑定到原始的 bar.py:a 对象(None)。这就是为什么你可以在 __init__.py 中使用 from bar import a as a2:在这种情况下,很明显你有 bar/bar.py:a 和一个 不同 的变量名 bar/__init__.py:a2(在你的例子中,这两个变量的名字恰好都是 a,但它们仍然存在于不同的命名空间中:在 __init__.py 中,它们是 bar.aa)。

现在,当你执行

import bar

print bar.a

时,你访问的是变量 bar/__init__.py:a(因为 import bar 导入了你的 bar/__init__.py)。这是你修改的变量(变成了 1)。你并没有触碰到变量 bar/bar.py:a 的内容。所以当你接下来执行

bar.foobar()

时,你调用的是 bar/bar.py:foobar(),它访问的是来自 bar/bar.py 的变量 a,这个变量仍然是 None(当 foobar() 被定义时,它一次性绑定了变量名,所以 bar.py 中的 abar.py:a,而不是其他模块中定义的任何 a 变量——因为在所有导入的模块中可能会有很多 a 变量)。因此最后输出的就是 None

总结:最好避免在 import bar 中出现任何歧义,方法是 不要 有任何 bar/bar.py 模块(因为 bar.__init__.py 已经使目录 bar/ 成为一个包,你也可以用 import bar 导入它)。

162

你在使用 from bar import a 这个语句。这样一来,a 就成了你当前模块中的一个全局符号(或者说是这个导入语句所在的作用域中的符号)。

当你给 a 赋一个新值时,其实只是改变了 a 指向的值,而不是改变它本身的值。你可以试着在 __init__.py 中直接用 import bar 来导入 bar.py,然后在这里进行实验,比如设置 bar.a = 1。这样,你实际上是在修改 bar.__dict__['a'],这才是上下文中 a 的“真实”值。

这个过程有点复杂,因为涉及到三层结构,但 bar.a = 1 其实是在修改名为 bar 的模块中的 a 的值,而这个模块是从 __init__.py 中派生出来的。它并不会改变 foobar 看到的 a 的值,因为 foobar 是在实际的文件 bar.py 中运行的。如果你想改变那个值,可以设置 bar.bar.a

使用 from foo import bar 这种导入方式有一个危险:它把 bar 分成了两个符号,一个是在 foo 中全局可见的,最开始指向原始值,另一个是在执行 import 语句的作用域中可见的。改变一个符号指向的地方,并不会改变它原本指向的值。

这种情况在尝试从交互式解释器中 reload 一个模块时会特别麻烦。

撰写回答