【问题标题】:How to properly export globals in Python modules?如何正确导出 Python 模块中的全局变量?
【发布时间】:2020-10-16 12:31:13
【问题描述】:

我在一个模块中看到了这种奇怪的行为,只有如果模块是使用 __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),而不是在包含两个文件的目录中,这也不会发生。

任何人都可以告诉我发生了什么以及 - 理想情况下 - 如何处理这种行为?

谢谢!

【问题讨论】:

    标签: python module directory global


    【解决方案1】:

    发生了两件事:

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

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

    您可以尝试这两个可以证明的事情,替换:

    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
    

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

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

    在您的函数中,您使用了 += 运算符,它将尝试就地添加,但 int 不(也不能)支持此操作(因为它是不可变类型:它无法更改到位)。 IE。创建一个新对象并重新分配名称以引用结果(在这种情况下a += 1a = 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 ... * 导入而创建名称时,会创建一个新名称。虽然最初两个名称都指向同一个对象,但当您调用 gutsinc_x 时,它会被重新分配并指向新的东西(添加的结果),而 module 中的另一个名称仍然指向原始对象(1 )。

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

    【讨论】:

    • 感谢解答,不过还是有一些后续: 1、为什么会有两个test_x变量?似乎从guts 导入它会创建一个同名的新变量,该变量恰好引用了与guts 中的变量相同的值。我不确定这是否真的发生了,但它肯定不符合我对“导入”事物的直觉。 2. 那么真的没有从多个文件中实现的模块中导出全局变量的好方法吗?或者我应该说,唯一可以合理导出的东西是不会改变的东西(它们的参考)?
    猜你喜欢
    • 2014-02-18
    • 1970-01-01
    • 2017-06-02
    • 2021-12-14
    • 2020-02-16
    • 1970-01-01
    • 2014-12-01
    • 1970-01-01
    相关资源
    最近更新 更多