python: 持有对 lambda 函数的引用的导入全局变量的状态

python: state of imported global variables holding a reference to lambda function

背景故事: 我正在尝试实现一种处理 -v 参数的方法,以增加应用程序的冗长程度。为此,我想使用一个全局变量,它最初指向一个空的 lambda 函数。如果给定 -v,则变量会更改并分配另一个 lambda 函数,该函数会打印它的输入。

MWE: 我注意到在通过 from x import *...

导入后从另一个模块调用 lambda 函数时,这没有按预期工作

mwe.py:

from mod import *
import mod

def f():
  vprint("test in f")

vprint("test before")
print("before: %d" % foo)
set_verbosity(1)
vprint("test after")
print("after: %d" % foo)
f()
mod.vprint("explicit: %d" % mod.foo)
modf()

mod.py:

vprint = lambda *a, **k: None
foo = 42

def set_verbosity(verbose):
  global vprint, foo
  if verbose > 0:
    vprint = lambda *args, **kwargs: print(*args, **kwargs)
    foo = 0

def modf():
  vprint("modf: %d" % foo)

输出为

before: 42
after: 42
explicit: 0
modf: 0

其中 "explicit" 和 "modf" 输出是由于 mwe 末尾的 mod.vprintmodf 调用。 vprint 的所有其他调用(通过 vprint 的导入版本)显然没有使用更新的定义。同样,foo 的值似乎只导入了一次。

问题: 在我看来,from x import * 类型的导入复制了导入模块的全局状态。我对解决方法本身不太感兴趣,但对这种行为的实际原因很感兴趣。这是在文档中的什么地方定义的?基本原理是什么?

解决方法: 作为旁注,无论如何实现这一点的一种方法是通过函数包装全局 lambda 变量并仅导出它们:

_vprint = lambda *a, **k: None

def vprint(*args, **kwargs):
  _vprint(*args, **kwargs)

def set_verbosity(verbose):
  global _vprint
  if verbose > 0:
    _vprint = lambda *args, **kwargs: print(*args, **kwargs)

这使得它与 import-from 方式一起工作,允许其他模块简单地调用 vprint 而不是通过模块名称显式引用。

前方黑夜中随机一刺:

If you look at the docs for __import__,有一点:

On the other hand, the statement from spam.ham import eggs, sausage as saus results in

_temp = __import__('spam.ham', globals(), locals(), ['eggs', 'sausage'], 0)
eggs = _temp.eggs
saus = _temp.sausage

我认为这是关键。如果我们推断 from mod import * 结果类似于:

_temp = __import__('mod', globals(), locals(), [], 0)
printv = _temp.printv
foo = _temp.foo

这表明问题所在。 printv是对旧版本printv的引用; mod.printv 在导入时指向的内容。重新分配 mod 中的 printv 指向的内容不会影响 mwe 中的任何内容,因为 mweprintv 的引用仍在查看之前的 lambda .

这和这个不变的方式类似b:

a = 1
b = a
a = 2

b 仍然指向 1,因为重新分配 a 不会影响 b 正在查看的内容。

另一方面,mod.printv确实有效,因为我们现在在mod中使用对全局的直接引用而不是指向的引用mod.

中的 printv

这是一个随机刺探,因为我想我根据前一段时间的一些随机阅读知道了答案。如果我不正确,请告诉我,我会删除它以避免混淆。

TL;DR:当您执行 from module import * 时,您是在 复制名称及其相关引用 ;更改与原始名称关联的引用不会更改与副本关联的引用。


这处理了 namesreferences 之间的根本区别。这种行为的根本原因与 python 处理此类事情的方式有关。

在python中只有一件事是真正不变的:记忆。您不能直接更改单个字节。但是,python 中的几乎所有内容都处理 单个字节的引用,您可以更改这些引用。当您执行 my_list[2] = 5 时,您并没有更改任何内存 - 相反,您正在分配一个新的内存块来保存值 5,并将 my_list 的第二个索引指向它。 my_list[2] 曾经指向的原始数据仍然存在,但由于不再引用它,垃圾收集器最终会处理它并释放它正在使用的内存。

同样的原则也适用于名字。 python 中的任何给定名称空间都与 dict 相当 - 每个名称都有相应的引用。这就是问题所在。

考虑以下两个语句之间的区别:

from module import *
import module

在这两种情况下,module 都被加载到内存中。

在后一种情况下,只有一件事被添加到本地命名空间——名称'module',它引用包含刚刚加载的模块的整个内存块。或者,嗯,它引用了该模块自己的命名空间的内存块,它本身引用了模块中的所有名称,依此类推。

然而,在前一种情况下,module 命名空间中的每个名称都被复制到本地命名空间中。同一个内存块仍然存在,但是现在 many[=] 不再是 oneall 的引用111=] 引用了它的 小部分


现在,假设我们连续执行这两个语句:

from module import *
import module

这给我们留下了一个名称 'module' 引用模块加载到的所有内存,以及一堆其他名称引用该块的各个部分。我们可以验证它们指向同一事物:

print(module.func_name == func_name)
# True

但是现在,我们尝试将其他内容分配给 module.attribute:

module.func_name = lambda x:pass
print(module.func_name == func_name)
# False

不一样了。为什么?

嗯,当我们做module.func_name = lambda x:pass的时候,我们首先分配了一些内存来存储lambda x:pass,然后我们改变了module'func_name'名称来引用那块内存而不是它所引用的内容。请注意,就像我之前给出的列表示例一样,我们没有更改 module.func_name 之前引用的内容 - 它仍然存在,并且本地 func_name 继续引用它。

因此,当您执行 from module import * 时,您是在 复制名称及其关联的引用 ;更改与原始名称关联的引用不会更改与副本关联的引用。


解决方法是不执行 import *。事实上,这几乎就是 为什么 使用 import * 通常被认为是不良做法的最终原因,除了少数特殊情况。考虑以下代码:

# module.py
variable = "Original"
# file1.py
import module

def func1():
    module.variable = "New"
# file2.py
import module
import file1

print(module.variable)
file1.func1()
print(module.variable)

当您 运行 python file2.py 时,您会得到以下输出:

Original
New

为什么?因为 file1file2 都导入了 module,并且在它们的两个命名空间中 'module' 都指向同一个内存块。 module 的命名空间包含引用某个值的名称 'variable'。然后,会发生以下事情:

  1. file2 说“好的,module,请给我在你的命名空间中与名称 'variable' 关联的值。
  2. file1.func1() 说“好的,module,您命名空间中的名称 'variable' 现在引用了这个其他值。
  3. file2 说“好的,module,请给我在你的命名空间中与名称 'variable' 关联的值。

由于 file1file2 仍在与同一位内存通信,因此它们保持协调。