← 返回题目列表

Python import 机制和模块缓存是怎样的?

高频 中等 第 12 / 27 题 更新于 2026/07/27
import模块缓存sys.modules

简化版

Python 导入模块时会先查 sys.modules 缓存,已有则直接复用;没有则按搜索路径查找、创建模块对象,通常先放入缓存,再执行模块代码。模块代码通常只在首次导入时执行一次,所以导入副作用和循环导入是常见面试点。

详细版

示例:

import sys
import json

print("json" in sys.modules)  # True

导入大致流程:

  1. 检查 sys.modules 是否已经有模块对象;
  2. 没有则根据 sys.path、包路径和导入器查找模块;
  3. 创建模块对象;
  4. 通常先把模块对象放入 sys.modules
  5. 执行模块顶层代码。

如果模块执行失败,导入系统会清理本次失败导入放入的缓存项。模块顶层代码只应放定义和轻量初始化,不要放重副作用逻辑:

# bad_module.py
print("importing")
connect_database()

循环导入问题通常来自两个模块在顶层互相导入,并且导入时立即访问对方尚未定义完成的名字。

解决思路包括:重构公共依赖、延迟导入、把副作用放进函数、减少模块顶层执行逻辑。

完整版教学

一、模块导入不只是复制代码

当你写:

import config

Python 会加载 config.py,执行里面的顶层代码,并创建一个模块对象。

模块对象有自己的命名空间:

config.DEBUG
config.HOST

这不是把 config.py 的文本复制到当前文件,而是当前模块拿到了另一个模块对象的引用。

二、sys.modules 缓存

sys.modules 是一个字典,保存已经加载过的模块:

import sys
print(sys.modules["json"])

当再次导入同一个模块:

import json
import json

Python 会直接复用缓存里的模块对象,不会重复执行模块顶层代码。

这也是为什么模块适合保存全局单例配置,但也要谨慎处理可变全局状态。

三、from import 和 import 的差异

import module

导入模块对象,使用时写:

module.name
from module import name

把模块里的某个名字绑定到当前命名空间。

如果模块后续重新绑定这个名字,当前模块里的绑定不一定跟着变化。

例如:

from settings import DEBUG

此时 DEBUG 是当前模块里的一个名字,和 settings.DEBUG 后续是否变化要看对象和绑定方式。

四、模块搜索路径

Python 查找模块时会参考 sys.path

常见来源:

  • 当前脚本所在目录;
  • 环境变量 PYTHONPATH
  • 标准库路径;
  • site-packages;
  • 虚拟环境路径。

如果出现导入错模块,常见原因是当前目录里有同名文件:

json.py
requests.py

这会遮蔽标准库或第三方库。

五、循环导入为什么发生

假设:

# a.py
from b import func_b
def func_a():
    ...
# b.py
from a import func_a
def func_b():
    ...

导入 a 时,a 去导入 bb 又去导入 a。此时 a 可能还没执行完,func_a 还没定义好,于是访问失败。

这类问题不是“不能循环引用文件”,而是模块初始化顺序导致某些名字还不存在。

六、如何解决循环导入

常见办法:

  • 抽出公共依赖到第三个模块;
  • 把导入放到函数内部延迟执行;
  • 减少模块顶层副作用;
  • 用依赖注入传入对象;
  • 重新划分模块职责。

延迟导入示例:

def func_a():
    from b import func_b
    return func_b()

这不是首选架构方案,但可以作为局部缓解。

七、常见误区与追问

易错点:import 执行的是模块初始化,并把模块对象放进 sys.modules。后续导入多半是在拿缓存,不是把文件重新跑一遍。

  • 误区:每次 import 都会重新执行模块文件。 第一次导入会创建并执行模块,之后同名模块通常直接从 sys.modules 取缓存对象。
  • 追问:为什么循环导入会拿到“半初始化模块”? Python 会先把模块对象放进 sys.modules 再执行模块代码;循环导入时,另一个模块可能看到尚未执行完的对象。
  • 误区:from module import nameimport module 完全一样。 前者把某个名字绑定到当前命名空间,后者绑定模块对象;模块里后续重新绑定该名字时,已导入的本地名字不一定同步变化。
  • 追问:importlib.reload() 能解决所有热更新吗? 不能。它会重新执行模块代码,但外部已经 from 出去的旧对象引用、已有实例和其他模块缓存不一定自动更新。
  • 误区:改 sys.path 是管理依赖的好方式。 动态改搜索路径容易造成环境差异和同名模块遮蔽;工程中应优先使用包结构、虚拟环境和标准安装方式。
  • 追问:如何减少循环导入? 抽出公共模块、移动局部导入到函数内部、反转依赖、把类型注解放到 if TYPE_CHECKING 中,都是常见解法。

八、加强记忆

import 的关键是“查缓存、找模块、创建模块、放入缓存、执行顶层代码”。模块首次导入会执行,后续从 sys.modules 复用。循环导入多半是顶层互相依赖导致读到了半初始化模块,解决方向是重构公共依赖、延迟导入和减少顶层副作用。