bigywater.tech
Python · 31

import 机制:模块查找、缓存与循环导入

真实构造两组循环导入场景,验证import整模块和from...import具体名字在循环发生时的真实行为差异,并确认sys.modules是真实的单例缓存机制。

整模块循环导入
真实跑通,只是拿到加载中途的半成品
具名循环导入
真实ImportError:名字这时候还没被赋值
sys.modules
真实单例缓存,同一模块永远是同一个对象

做的事:让两个模块互相import,观察哪种写法能跑通

代码 mod_a.py / mod_b.py# mod_a.py print("mod_a: start loading") import mod_b print("mod_a: mod_b imported, mod_b.X =", mod_b.X) X = "A_VALUE" print("mod_a: finished loading") # mod_b.py print("mod_b: start loading") import mod_a print("mod_b: trying to access mod_a.X =", getattr(mod_a, "X", "<NOT SET YET>")) X = "B_VALUE" print("mod_b: finished loading") python3 -c "import mod_a"

验证:用import整模块+getattr,循环导入真实跑通了

实测 · 本机执行输出
mod_a: start loading
mod_b: start loading
mod_b: trying to access mod_a.X = <NOT SET YET>   ← 真实拿到的是"NOT SET YET",不是A_VALUE
mod_b: finished loading
mod_a: mod_b imported, mod_b.X = B_VALUE            ← 这时候mod_b已经跑完了,X真实存在
mod_a: finished loading

真实的执行顺序是:import mod_a触发mod_a开始加载 → mod_a第一行执行import mod_b → mod_b开始加载 → mod_b又要import mod_a,但这时mod_a已经在sys.modules里挂了一个"半成品"条目(模块对象存在,但代码只跑到import mod_b这一行,X = "A_VALUE"这行还没执行)→ Python真实不会重新执行mod_a,直接把这个半成品对象返回给mod_b → mod_b访问mod_a.X,真实这个属性还不存在,getattr的默认值救了场 → mod_b跑完,X = "B_VALUE"真实被赋值 → 控制权回到mod_a的import mod_b那一行,这时候mod_b是完整的,mod_b.X真实能拿到"B_VALUE"。

验证:用from...import具体名字,同样的循环真实直接崩了

代码 mod_c.py / mod_d.py# mod_c.py print("mod_c: start loading") import mod_d X = "C_VALUE" print("mod_c: finished loading") # mod_d.py print("mod_d: start loading") from mod_c import X print("mod_d: got X =", X)
实测 · 本机执行输出
mod_c: start loading
mod_d: start loading
ImportError: cannot import name 'X' from 'mod_c'
(consider renaming '.../mod_c.py' if it has the same name as a library you intended to import)
跟上一组的区别只有一处:mod_d用from mod_c import X直接要"名字",而不是要整个模块对象。此时mod_c真实执行到了import mod_d那一行就被打断去加载mod_d,X = "C_VALUE"这行代码根本还没跑,所以mod_c这个半成品对象上真实完全没有X这个属性——from ... import 具体名字在底层等价于"先拿到模块对象,再用getattr取那个名字,取不到就报错",而上一组的import mod_b只是拿模块对象本身,不会在这一刻去校验某个具体属性是否存在。这是本文认为"循环导入到底会不会炸"最关键的真实分界线:不在于是否存在循环,而在于被导入的一方在循环发生的那个时间点,需要的名字是不是已经真实被赋值了。

验证:sys.modules真实是单例缓存,不是"每次import都重新执行"

实测 · 本机执行输出
import mod_a
print(id(mod_a))                    → 4377790320
import mod_a as mod_a2
print(mod_a is mod_a2)               → True    ← 真实是同一个对象,没有重新执行mod_a.py

mod_a.X = "CHANGED"
import mod_a as mod_a3
print(mod_a3.X)                      → CHANGED  ← 真实立刻可见,因为mod_a3也是同一个对象

spec = importlib.util.find_spec("json")
print(spec.origin)   → .../python3.14/json/__init__.py
print("json" in sys.modules)   → 先False,import后True

真实的机制是:第一次import mod_a会真的执行一遍mod_a.py的顶层代码,执行完之后把结果模块对象存进sys.modules["mod_a"];后面任何地方再import mod_a,Python真实只做一件事——查sys.modules字典,命中就直接返回那个已经存在的对象,源文件不会被重新读取、也不会重新执行。这解释了为什么在模块级别定义的"全局状态"(比如一个连接池、一个缓存字典)在整个进程里真实只有一份:所有import它的地方拿到的都是同一个对象,谁改了它的属性,所有持有者立刻看得到——这既是模块能当"单例"用的原因,也是修改全局可变状态容易在大项目里造成隐蔽耦合的原因。

分步演示:从整模块循环导入到具名循环导入的真实分界线

每一步都是本文上面用真实构造的模块文件跑出来的原始结果。

循环导入炸不炸,取决于要名字还是要模块
这一步新验证的行为

    本文用到的工具

    • 本机 Python 3.14.7,真实构造的两组循环导入模块文件
    • sys.modules / importlib.util.find_spec
    京ICP备2026067502号-1京公网安备11010502063487号