Python 导入机制和模块模拟

Liv*_*viu 6 python unit-testing python-3.x python-importlib

这更多是为了理解 Python(在本例中为 3.9)的工作原理,而不是解决实际问题的努力,所以请耐心等待并忽略 m3 的荒谬方式。我只是想复制我正在处理的东西。

我有以下结构:

??? m1.py
??? m2
    ??? m3
        ??? __init__.py
        ??? m3.py
Run Code Online (Sandbox Code Playgroud)

m2/m3/初始化.py:

??? m1.py
??? m2
    ??? m3
        ??? __init__.py
        ??? m3.py
Run Code Online (Sandbox Code Playgroud)

m2/m3/m3.py:

from .m3 import *
Run Code Online (Sandbox Code Playgroud)

从现在开始,我将对 m1.py 进行更改

这是有效的,我期待它的工作:

def m3func():
    print('m3 func is here')
Run Code Online (Sandbox Code Playgroud)

这并没有失败,所以它替换了 Mock 的模块。我也期待它的工作方式。

import m2.m3
m2.m3.m3func()
Run Code Online (Sandbox Code Playgroud)

同样的

import sys
from unittest.mock import Mock
sys.modules['m2.m3'] = Mock()
import m2.m3 as alias
alias.m3func()
Run Code Online (Sandbox Code Playgroud)

我不明白这里发生了什么:

import sys
from unittest.mock import Mock
sys.modules['m2.m3'] = Mock()
from m2 import m3
m3.m3func()
Run Code Online (Sandbox Code Playgroud)
m2.m3.m3func()
AttributeError: module 'm2' has no attribute 'm3'
Run Code Online (Sandbox Code Playgroud)

之间有什么区别import m2.m3from m2 import m3还有import m2.m3 as alias 什么我不明白,有没有办法修复最后一个版本,这样它就不会抛出 AttributeError?我的示例 m2 是空的,但实际上,我不想完全换掉它,因为它确实包含我关心的东西。我只想针对 m3。是否有一个建议,尽可能最佳实践去使用这样的代码:m2.m3.m3func()

小智 3

如果您还没有阅读过官方文档,您绝对应该阅读。

每当您使用 import 语句时,都会发生以下情况:

  1. Python 搜索模块,并处理一路上发现的新模块
  2. Python 引入一个或多个变量来使用导入的任何内容

发现问题

Python 在内部使用它sys.modules来搜索模块,并在发现模块和父模块时更新其条目。因此,当您 import 时m2.m3.m3,它会添加带有键“m2”、“m2.m3”、“m2.m3.m3”的条目来存储引用这些模块的对象。您可以使用以下函数sys.modules在每个语句之前/之后进行调试。

def print_status_relevant_modules():
    import sys
    print(sys.modules.get("m2", "<m2 not loaded>"))
    print(sys.modules.get("m2.m3", "<m2.m3 not loaded>"))
    print(sys.modules.get("m2.m3.m3", "<m2.m3.m3 not loaded>"))
    print()
Run Code Online (Sandbox Code Playgroud)

每当 Python 发现一个新包(__init__.py其中包含文件的目录)或module.py模块时,它也会处理该模块。在这种情况下,当Python发现模块 的存在时m2.m3,就会执行 的内容m2/m3/__init__.py,从而执行 import 语句from .m3 import *,从而导致模块的发现m2.m3.m3(并引入局部变量m3funcm2.m3

当开始模拟 sys.modules 中带有键“m2.m3”的条目时,您的问题就开始了,因为现在您已经中断了 Python 搜索模块的过程。因为你事先模拟了 sys.modules 中模块的条目m2.m3,Python 认为它已经处理了这个模块,所以 Python 永远不会执行它的__init__.py文件。结果,m2.m3.m3永远不会被发现,不会添加任何条目,并且 for 的局部变量m3func永远不会被引入到 中m2.m3

如果您想知道为什么在模拟模块的条目时没有看到任何错误,即使您正在调用m3func(),这是因为模拟将接受任何调用,期望您稍后验证是否进行了某个调用。

不同的进口声明

所有不同 import 语句之间的最大区别在于引入了哪些局部变量:

  • import m2.m3结果是一个m2带有属性的局部变量m3;引入的变量引用代表模块的 Module 实例m2
  • from m2 import m3结果为局部变量m3;引入的变量引用代表模块的 Module 实例m2.m3
  • import m2.m3 as alias结果为局部变量alias;引入的变量引用代表模块的 Module 实例m2.m3

您可以在任何地方使用该语句print(dir())来查看定义了哪些局部变量,或者在对象上使用该语句。

print(dir())    # m2 is not defined
import m2.m3
print(dir())    # m2 is defined
print(dir(m2))  # shows that m2 has an attribute m3
Run Code Online (Sandbox Code Playgroud)

作为额外的好处,语句from .m3 import *inm2/m3/__init__.py会导致 的所有局部变量m2.m3.m3被导入到m2.m3m3func在这种情况下,仅添加变量。

当使用 import 语句时,会引入import m2.m3局部变量,该变量引用代表模块 m2 的实例。在搜索模块时,Python 应该已经发现了模块 m2.m3,并将该属性添加到表示模块 m2 的实例中,以引用表示模块 m2.m3 的实例。但是,因为您事先进行了模拟,所以永远不会发现模块 m2.m3,因此该属性永远不会添加到表示模块 m2 的实例中。当您尝试访问.m2Modulem3ModuleModulesys.modules['m2.m3']m3Modulem2.m3

当您使用 import 语句时,引入的import m2.m3 as alias局部变量引用代表模块 m3 的实例。但是,因为您事先进行了模拟,Python 认为它已经发现了模块 m2.m3,并返回 的值。因此,变量最终引用实例而不是表示模块 m2.m3 的实例,并且您不会收到任何错误,因为实例接受所有调用。aliasModulesys.modules['m2.m3']sys.modules['m2.m3']aliasMockModuleMock

当你使用 import 语句时也会发生同样的事情from m2 import m3;该变量m3最终将引用该Mock实例。

如何解决您的问题

据我所知,你把Python的导入系统弄乱了,以至于你不能再依赖它“只使用”m2.m3m2.m3.m3. Python 会找到一种方式以这样或那样的方式进行抱怨。

这可能是一种情况,实际问题是设计问题,而模拟永远不会是正确的答案,从长远来看只会导致更多问题,但是,我不知道实际情况是什么。但是,您应该尝试找到一种方法来避免整个情况。