如何在 Python 模块中正确导出全局变量



我在模块上看到了这种奇怪的行为,只有当模块是使用__init__.py和另一个文件实现的,其中包含模块的"胆量"时,才会发生这种情况。所以,假设我有一个名为module的模块,在module目录中实现。此目录包含两个文件:

  1. __init__.py
# contents of module/__init__.py
from .guts import *
  1. 同一目录中也有guts.py
# contents of module/guts.py
test_x = 1
def inc_x():
global test_x
test_x += 1
def print_x(prefix):
print(f"{prefix} in module: {test_x}")

现在,我正在尝试使用此模块,包括其来自应用程序的全局test_x

x = module.test_x
print(f"before in test  : {module.test_x}")
module.print_x("before")
print("incrementing...")
module.inc_x()
print(f"after  in test  : {module.test_x}")
module.print_x("after ")
assert x==module.test_x-1
print("SUCCESS!!n")

如果你在python下执行了这段代码(我使用的是3.7.6),你会得到以下输出:

before in test  : 1
before in module: 1
incrementing...
after  in test  : 1
after  in module: 2
Traceback (most recent call last):
File "test.py", line 9, in <module>
assert x==module.test_x-1
AssertionError

我对这种行为完全感到困惑,就好像test_x有一个分裂的人格:一个在模块内部,一个在模块外。

注意:如果我将guts.py的内容复制到__init__.py而不是导入它,则不会发生这种情况。

注意:如果模块作为单个文件(module.py)实现,而不是在包含两个文件的目录中实现,则也不会发生这种情况。

谁能启发我正在发生的事情,以及 - 理想情况下 - 如何处理这种行为?

谢谢!

发生了两件事:

  • guts导入*时,test_x被导入到module的命名空间中,并且值1
  • 当您运行inc_x时,您将声明test_x全局,即在其模块(guts)中是全局的。

现在,由于int是不可变的(并且不/不能支持就地递增),它最终/有效地最终将2(递增的值)分配给全局gutstest_x

您可以尝试以下两件事,它们可能会证明这一点,请替换:

print(f"after  in test  : {module.test_x}")

跟:

import module.guts
print(f"after  in test  : {module.guts.test_x}")

执行相同操作后,module.test_x为 1,但module.guts.test_x按预期显示递增的值。

您还可以将guts更改为如下所示:

test_x = [1]
def inc_x():
global test_x
test_x[0] += 1
def print_x(prefix):
print(f"{prefix} in module: {test_x[0]}")

并测试匹配:

x = module.test_x[0]
print(f"before in test  : {module.test_x[0]}")
module.print_x("before")
print("incrementing...")
module.inc_x()
print(f"after  in test  : {module.test_x[0]}")
module.print_x("after ")
assert x==module.test_x[0]-1
print("SUCCESS!!n")

现在,当您导入guts时,module.test_x是一个列表(并且与module.guts.test_x列表完全相同)。当您操作该列表中的项目时,您仍在访问同一实例,无论它是通过guts.test_x(也作为其全局内部inc_x)还是通过module.test_x。-> 您不为guts.test_x赋值;您更改了module.test_x引用的对象。

也就是说,我通常对global和正确有些怀疑。是的,它们是混淆任何阅读代码的人的好方法。


关于您的进一步询问:每个对象都有其标识(并驻留在内存中的某个位置)。变量(名称)是对这些对象的引用。赋值给变量会建立对此对象的引用:

>>> class C: pass
... 
>>> a = C()  # create new instance and assign it to a
>>> b = a    # assign that instance to b
>>> c = C()  # create new instance and assign it to c
>>> id(a) == id(b)   # or: a is b
True
>>> id(a) == id(c)   # or: a is c
False

ab都是不同的名称,指的是同一个对象。

>>> a.a = 1
>>> print(b.a)
1

然而:

>>> a = 1
>>> b = a
>>> id(a) == id(b)   # or: a is b
True 
>>> b = 2
>>> id(a) == id(b)   # or: a is b
False
>>> a
1

这里发生了什么?我有一个对象(值1的文字int),我已经a分配了一个名称(变量),然后我也b引用了同一个对象。但是,对于b = 2,我没有更改对象的值,而是重新分配b引用的内容(具有值2的文字int)。

在第一个示例中,更改class C实例的属性a。我正在就地修改对象...两者都一直指向同一个对象(实例)。在第二个示例中。我已经更改(重新分配)了引用和ab不再指向同一对象。

在您的函数中,您使用了+=运算符,它将尝试就地添加,但此操作不受(也不能)int支持(因为它是不可变的类型:它不能就地更改)。 即创建一个新对象并重新分配名称以引用结果(在这种情况下a += 1具有与a = a + 1相同的效果)。

现在,即使你的变量(名称)被global,它仍然只是在其模块中是全局的。您可以使用globals、两个小文件m1.py

script_a = 1
# prune keys starting with "__" from printed dict
print("in m1:", {k: v for (k, v) in globals().items() if not k.startswith("__")})

script.py

import m1
m1_a = 1
print("in script:", {k: v for (k, v) in globals().items() if not k.startswith("__")})

会给你:

$ python3 script.py 
in m1: {'script_a': 1}
in script: {'m1': <module 'm1' from '/tmp/m1.py'>, 'm1_a': 1}

这意味着,当由于导入而创建名称时from ... *会创建一个新名称。虽然最初两个名称都指向同一个对象,但当您调用inc_x时,guts会被重新分配并引用新的东西(添加的结果),而module中的另一个名称仍然指向原始对象(1)。

现在正如所暗示的那样。我通常不鼓励使用global变量,因为它们可能会导致不太明显的行为,并且会使将来阅读和维护脚本变得更加困难。也就是说,虽然可以通过使用类型(例如list)并就地修改对象来获得所需的行为。跨越模块边界会将其带到另一个令人困惑的层次。确实很难立即看到代码一部分更改的影响以及(可能是不可预见的)它在其他地方可能产生的影响(其中来自同一模块的导入也被导入并在更大的脚本中使用)。

相关内容

  • 没有找到相关文章

最新更新