模块导入循环
导入循环的形成
两个模块互相导入,就形成了导入循环,比如下面的例子:
mod1:
import mod2
s1 = 'in mod1'
print(s1)
mod2:
import mod1
s2 = 'in mod2'
print(s2)
执行 mod1 ,不会有任何报错,代码正常执行。
现在将 import 语句修改为 from ... import ... 语句:
mod1:
from mod2 import s2
s1 = 'in mod1'
print(s1)
mod2:
from mod1 import s1
s2 = 'in mod2'
print(s2)
这时再执行 mod1,就会报错 ImportError: cannot import name 's2'
。这是因为此情况下,当模块被导入时,模块中的名称还未定义。
import 语句剖析
要弄明白这个原因,需要捋一捋 Python 解释器处理 import 语句的流程:
-
首先解释器会检查 sys.modules(sys.modules 是一个字典,用于缓存之前导入的模块):
- 如果在 sys.modules 内找到了要导入的模块,那么直接将模块名添加到当前的名称空间,完成导入
- 如果没有找到,那么继续下面的流程
-
调用 Python 的导入协议——查找器和加载器
-
搜索 sys.meta_path,sys.mata_path 是包含元路径查找器对象的列表。Python 内有多种查找器:
- 知道如何导入内置模块
- 知道如何导入冻结模块
- 知道如何导入来自 import path 的模块
这些查找器会被按顺序查询以确定它们是否知道如何处理该名称的模块:
- 如果元路径查找器知道如何处理指定名称的模块,它将返回一个说明对象(内含加载器)
- 如果不能处理,则会返回 None
如果 sys.meta_path 处理过程到达列表末尾仍未返回说明对象,则引发 ModuleNotFoundError。任何其他被引发异常将向上传播,并放弃导入过程。
当一个模块说明被找到时,导入机制将在加载模块时使用它(及其所含的加载器)。
在模块被加载前,模块已经存在于 sys.modules 中,这可以防止无限递归或多次加载。
在模块创建完成但是还未执行之前,导入机制会设置导入相关模块属性(__name__ ,__loader__等)
-
模块加载器提供关键的加载功能——模块执行
- 加载器会在模块的全局名称空间(module.__dict__)中执行模块的代码,并填充模块的名称空间
- 如果加载器无法执行执行模块,它将引发 ImportError,模块也会从 sys.modules 中移除
-
将模块名导入到当前名称空间中,完成导入
画个流程图看下:
Python 解释器处理 import 语句的流程循环导入分析
import 语句
回到开始的例子,来分析下循环导入发生了什么
mod1:
import mod2
s1 = 'in mod1'
print(s1)
mod2:
import mod1
s2 = 'in mod2'
print(s2)
按照执行步骤画个流程图:
导入循环的执行.png按照流程图,我们可以推导出执行的结果是依次打印:
in mod1
in mod2
in mod1
和终端中运行 mod1 的结果相同。
from ... import ...
再来看报错的例子,用同样的方法分析下。
mod1:
from mod2 import s2
s1 = 'in mod1'
print(s1)
mod2:
from mod1 inport s1
s2 = 'in mod2'
print(s2)
注意,from module import item
语句在执行的时候,会先导入 module, 再引用item,这一点很重要。
流程图:
导入循环的执行——报错.png根据流程图可以看到,在整个流程中并没有执行 s2 = 'in mod2'
,即 s2 尚未定义,所以导致了最后的报错。
根据执行流程,我们还可以推断:
-
在 mod1 中
import mod2
, 在 mod2 中from mod1 import s1
,不会导致报错。但这并没有什么用,因为这种情况下 mod1 无法引用 mod2 内的名称,比如 mod1:import mod2 s1 = 'in mod1' print(s1) print(mod2.s2)
-
同样的道理,mod1 内
import mod2
,mod2 内import mod1
,虽然不报错,也没卵用 -
如果将导入语句放在变量定义之后,不会报错
-
但是,互相导入特殊的名称(__name__,__loader__等)不会报错,因为特殊的名称在模块加载之前就已经设置
解决方案
对于模块导入循环,最佳的解决方案还是尽量避免两个模块互相导入。如果互相导入不可避免,那需要将导入语句放在变量定义之后。BTY,将 from ... import ...
语句放在函数定义中也是一种方案,但是函数必须要在变量定义之后执行,否则也是会报错的。