真实构造两组循环导入场景,验证import整模块和from...import具体名字在循环发生时的真实行为差异,并确认sys.modules是真实的单例缓存机制。
代码 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"
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"。
代码 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)
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它的地方拿到的都是同一个对象,谁改了它的属性,所有持有者立刻看得到——这既是模块能当"单例"用的原因,也是修改全局可变状态容易在大项目里造成隐蔽耦合的原因。
每一步都是本文上面用真实构造的模块文件跑出来的原始结果。
sys.modules / importlib.util.find_spec