Python 导入机制和模块模拟

Python import mechanism and module mocks

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

我有以下结构:

├── m1.py
└── m2
    └── m3
        ├── __init__.py
        └── m3.py

m2/m3/init.py:

from .m3 import *

m2/m3/m3.py:

def m3func():
    print('m3 func is here')

从现在开始,我将对 m1.py

进行更改

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

import m2.m3
m2.m3.m3func()

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

import sys
from unittest.mock import Mock
sys.modules['m2.m3'] = Mock()
import m2.m3 as alias
alias.m3func()

这个也一样

import sys
from unittest.mock import Mock
sys.modules['m2.m3'] = Mock()
from m2 import m3
m3.m3func()

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

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

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

你绝对应该 read the official documentation,如果你还没有的话。

只要您使用 import 语句,就会发生以下情况:

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

发现问题

Python 在内部使用 sys.modules 来搜索模块,并在沿途找到模块和父模块时更新其条目。因此,当您导入 m2.m3.m3 时,它会添加带有关键字 'm2'、'm2.m3'、'm2.m3.m3' 的条目来存储引用这些模块的对象。您可以使用以下函数调试 sys.modules before/after 每个语句。

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()

每当 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永远不会被发现,不会添加任何条目,m3func的局部变量也永远不会被引入m2.m3

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

不同的导入语句

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

  • import m2.m3 导致具有属性 m3 的局部变量 m2;引入的变量是指表示模块 m2
  • 的 Module 实例
  • from m2 import m3 导致局部变量 m3;引入的变量是指表示模块 m2.m3
  • 的 Module 实例
  • import m2.m3 as alias 导致局部变量 alias;引入的变量是指表示模块 m2.m3
  • 的 Module 实例

您可以在任何地方或在对象上使用语句 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

作为额外的奖励,m2/m3/__init__.py 中的语句 from .m3 import * 导致 m2.m3.m3 的所有局部变量都被导入到 m2.m3。在这种情况下,只添加了变量m3func

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

时,这最终会导致错误

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

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

如何解决您的问题

据我所知,你把 Python 的导入系统搞砸了,以至于你不能再依赖它来“只使用”m2.m3m2.m3.m3。 Python 会想办法以这种或那种方式抱怨。

这可能是实际问题是设计问题的情况,mocking 永远不会是正确的答案,而且只会在很长一段时间内造成更多问题 运行,但是,我不知道实际情况是怎样的。但是,您应该尝试找到一种避免这种情况的方法。