本节摘要:import = 查找 → 执行 → 缓存 → 绑定。模块只执行一次、模块即命名空间对象、包是带
__init__的目录——这三条机制推出本节全部结论,包括循环导入这个高频疑难杂症的病理与疗法。
import json 执行时,解释器走四个阶段:
sys.modules 是全局字典,键为模块名。命中则直接进入第 4 步——这就是"模块只执行一次"的原因;sys.path 的目录顺序找同名模块或包。找不到即 ModuleNotFoundError;__dict__;sys.modules 的缓存可以亲眼验证:
>>> import sys >>> import json >>> sys.modules["json"] is json True >>> import importlib >>> importlib.reload(json) # 显式重载才会重新执行(仅开发调试用)
模块是对象这句话也有实锤——第 1.2 节的工具再次上场:
>>> type(json) <class 'module'> >>> json.__dict__.keys() dict_keys(['__name__', 'dumps', 'loads', '__file__', ...])
一个 .py 文件被导入两次(比如既 import x 又 from x import y),模块体只执行一次,两个语句拿的是同一个对象——顶层打印语句不会重复出现,全局状态也只有一份。

__name__ 与入口约定每个模块都有 __name__:被导入时是模块名,被直接运行时是 "__main__"。这解释了那个全民约定的由来:
def main(): ... if __name__ == "__main__": # 直接运行才执行;被导入时只暴露定义 main()
它既是"可执行脚本"与"可导入模块"的分界线,也是 python -m 包名 启动方式的基石——-m 把包当 __main__ 运行,pip、http.server 都是这么启动的。
包是含 __init__.py 的目录;子目录继续含则是子包。导入包时先执行包的 __init__.py——它是包的"门面":常用做法在这里提升便捷接口:
# shapes/__init__.py 内容示例 from .circle import Circle # 相对导入:. 表示当前包 from .square import Square
于是使用方 from shapes import Circle 一步到位,无需了解内部文件布局——公开 API 与内部结构解耦,这是 __init__ 的核心价值。相对导入(. 当前包、.. 上级包)只能在包内使用;顶层脚本里写相对导入会报 ImportError: attempted relative import with no known parent package。
没有 __init__.py 的目录也能当包用,称为命名空间包——多个目录可拼成同一个包,适合插件式的大型项目;普通项目还是老老实实建 __init__.py,免得工具链(部分测试框架、类型检查器)犯迷糊。
两个模块在顶层互相 import,就形成环。先看病灶:
# a.py import b def fa(): return b.fb() # b.py import a def fb(): return "b ok" def fc(): return a.fa()
import a 时:a 开始执行 → 遇 import b → b 开始执行 → 遇 import a → 缓存里已有"半成品 a"(第 1 阶段就注册了)→ b 拿到半成品继续跑完 → a 继续跑完。这个例子能侥幸工作,因为互相引用的只是函数名(调用时才查找)。把 import b 换成 from b import fb 立刻翻车:
ImportError: cannot import name 'fb' from partially initialized module 'b' (most likely due to a circular import)
报错信息把病理说得很清楚:b 还在初始化中,fb 尚未定义。三种疗法按优先级:
import b + b.fb() 比 from b import fb 容忍度高得多。⚠️ 常见坑:脚本名与标准库撞名(第 1 章埋过伏笔,这里看清了机制——搜索路径第一站就是你所在目录);
pip install成功却ModuleNotFoundError,九成是装进了另一个解释器的 site-packages,用python -m pip install保证装进当前解释器;__init__.py里写重逻辑拖慢全家导入。
💡 关键直觉:把 import 看成"触发一次带缓存的模块执行",而不是"复制代码进来"。缓存、半成品、只执行一次——循环导入的所有怪象都能用这三条推演。
相对导入的规则可以再收紧成三条实操军规。第一,相对导入只在包内合法——顶层脚本(直接运行的那个文件)没有"当前包"的概念,硬写相对导入会在启动时直接报错,报错信息里的"没有已知的父包"说的就是这件事。第二,相对基准是文件的 __package__ 而不是文件所在目录——用 -m 方式启动包内的模块时相对导入正常,双击或直接指定文件运行时同一个文件就会炸,同一个文件两种命运,根子就在启动方式决定了包身份。第三,相对导入的层数要数得清:一个点是当前包,两个点是上一级,三个点以上基本说明项目结构该重构了。工程上规避这一整类问题的办法很朴素:项目从一开始就按"包 + 入口脚本分离"组织——一个 src 目录装包,一个薄薄的入口文件只做 import 与调用,相对导入永远活在包内,入口文件永远用绝对导入。这两种导入混用的边界清楚了,循环导入之外的第二类导入疑难也就绝缘了。
sys.modules 缓存 → 沿 sys.path 查找 → 执行模块体 → 绑定名字;模块只执行一次。__dict__ 里,type(module) 就是 module。__name__ 分界线:直接运行是 __main__,入口约定与 -m 启动的机制基础。__init__:门面模块做接口提升;相对导入限包内;普通项目别省 __init__.py。python -m pip 对齐。下一节解决"第三方包从哪来、装到哪、如何互不打架"——虚拟环境与依赖管理。