我在模块上看到了这种奇怪的行为,只有当模块是使用__init__.py
和另一个文件实现的,其中包含模块的"胆量"时,才会发生这种情况。所以,假设我有一个名为module
的模块,在module
目录中实现。此目录包含两个文件:
__init__.py
# contents of module/__init__.py
from .guts import *
- 同一目录中也有
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
(递增的值)分配给全局guts
test_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
a
和b
都是不同的名称,指的是同一个对象。
>>> 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
。我正在就地修改对象...两者都一直指向同一个对象(实例)。在第二个示例中。我已经更改(重新分配)了引用和a
,b
不再指向同一对象。
在您的函数中,您使用了+=
运算符,它将尝试就地添加,但此操作不受(也不能)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
)并就地修改对象来获得所需的行为。跨越模块边界会将其带到另一个令人困惑的层次。确实很难立即看到代码一部分更改的影响以及(可能是不可预见的)它在其他地方可能产生的影响(其中来自同一模块的导入也被导入并在更大的脚本中使用)。