Python import 到底做了什么?从 `sys.modules`、循环导入到插件加载与 5 秒冷启动诊断
2026/8/23 22:44:52 网站建设 项目流程

Python import 到底做了什么?从sys.modules、循环导入到插件加载与 5 秒冷启动诊断

很多 Python 开发者第一次接触import时,会自然地把它理解为一句很简单的话:

“把另一个.py文件拿过来用。”

这个理解不能说错,但只覆盖了表面。

在真实的 Python 工程里,import背后牵涉到模块缓存、路径搜索、模块创建、代码执行、命名空间绑定、Finder、Loader、ModuleSpec 以及动态加载机制。一旦项目规模扩大,这些机制会直接影响架构设计、循环依赖、插件系统,甚至服务的冷启动性能。

比如这两行代码:

importfooimportfoo

为什么foo.py里的代码通常只执行一次?

再比如:

ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import)

所谓partially initialized module到底“初始化了一半”是什么意思?

还有一个更现实的问题:

一个大型 Python 服务冷启动需要 8 秒,profiling 后发现其中 5 秒都花在 import 上,应该从哪里下手?

这篇文章就从这一句最普通的import foo出发,把 Python 导入系统完整拆开。


一、先做一个实验:为什么第二次import foo不重新执行?

创建foo.py

print("foo.py 正在执行")VALUE=42defhello():print("hello")

然后创建main.py

importfooimportfooprint(foo.VALUE)

执行:

python main.py

结果通常是:

foo.py 正在执行 42

注意:

foo.py 正在执行

只出现了一次。

原因并不是 Python 看到“上一行刚 import 过”,而是 Python 维护着一张非常重要的模块缓存:

sys.modules

第一次:

importfoo

完成后,大致会产生这样的状态:

importsysprint(sys.modules["foo"])

输出类似:

<module 'foo' from '/project/foo.py'>

也就是说:

sys.modules ↓ { "foo": <module foo> }

第二次执行:

importfoo

Python 首先检查:

"foo"insys.modules

发现模块已经存在,于是直接复用已有模块对象,而不会再次执行foo.py的顶层代码。

Python 官方导入系统文档也明确说明:导入搜索首先检查sys.modules;如果对应模块已经存在,通常直接返回缓存中的模块对象。(Python documentation)


二、sys.modules到底是什么?

可以把:

sys.modules

理解成 Python 解释器当前进程中的:

模块对象注册表 + 一级 import 缓存。

它本质上是一个字典。

importsysprint(type(sys.modules))

输出:

<class'dict'>

键是完整模块名:

"json""os""collections""package.submodule"

值是对应的模块对象。

例如:

importjsonimportsysprint(sys.modules["json"]isjson)

结果:

True

甚至可以观察两次 import 得到的对象:

importfoo first=fooimportfoo second=fooprint(firstissecond)

输出:

True

所以更准确地说:

importfoo

并不意味着:

每次都重新读取 foo.py。

而更接近:

if"foo"insys.modules:foo=sys.modules["foo"]else:# 搜索、创建并执行 foo...

当然,真实实现复杂得多。


可以手动删除sys.modules吗?

技术上可以:

importsysimportfoodelsys.modules["foo"]importfoo

这时 Python 会重新进行一次模块搜索和加载。

但是不要把它当作普通的“重新加载模块”方案。

因为:

old_foo=foodelsys.modules["foo"]importfooasnew_fooprint(old_fooisnew_foo)

通常得到:

False

旧模块对象并不会因为删除sys.modules项就自动消失,因为其他地方可能仍然持有它的引用。

如果真的需要重新执行模块,更合适的是:

importimportlibimportfoo importlib.reload(foo)

即便如此,生产系统中也应该慎用reload():已有实例、from x import y导入出来的旧引用、C 扩展状态等,都可能带来非常微妙的问题。


三、一条import foo背后究竟发生了什么?

忽略部分边界情况后,可以把 Python import 的核心流程理解为:

import foo │ ▼ 检查 sys.modules │ ├── 已存在 ──────► 直接复用模块 │ ▼ 通过 Finder 查找模块 │ ▼ 得到 ModuleSpec │ ▼ 创建 module 对象 │ ▼ 提前加入 sys.modules │ ▼ Loader 执行模块代码 │ ▼ 模块初始化完成 │ ▼ 把名称 foo 绑定到当前命名空间

其中最容易被忽视的一步是:

创建模块 ↓ 加入 sys.modules ↓ 执行模块代码

不是:

执行完代码 ↓ 再放进 sys.modules

这一点极其重要。

官方文档给出的导入过程伪代码,本质上就是:

module=create_module(...)sys.modules[name]=moduletry:loader.exec_module(module)exceptBaseException:delsys.modules[name]raise

也就是说,模块的代码还没执行完,它就已经出现在sys.modules中了。

这是循环导入问题的核心。(Python documentation)


四、为什么循环导入会出现partially initialized module

假设有两个文件。

user.py

fromorderimportcreate_orderclassUser:pass

order.py

fromuserimportUserdefcreate_order():returnUser()

现在执行:

importuser

发生了什么?


第一步:开始加载user

Python 创建:

module user

然后先登记

sys.modules["user"]=user

但是此时:

classUser:...

还没有执行。

于是现在的user模块可以近似看成:

user.__dict__={"__name__":"user",...}

里面暂时没有:

User

第二步:执行到from order import create_order

Python 开始加载order.py

同样:

sys.modules["order"]=order

然后执行:

fromuserimportUser

第三步:再次寻找user

Python一查:

"user"insys.modules

结果是:

True

于是不会重新执行user.py

问题来了。

当前user虽然已经存在,但:

classUser:

那一行还没运行。

于是:

user.User

不存在。

这就是:

partially initialized module

它不是一个神秘状态。

翻译成人话就是:

模块对象已经创建并注册了,但模块顶层代码还没有执行完成。


五、循环导入怎么解决?

不要看到循环导入就机械地把import移到函数里面。真正应该问的是:

为什么模块 A 和模块 B 在架构上互相依赖?

常见解决方案有四种。

方案一:提取共同依赖

原来:

user.py ───► order.py ▲ │ └───────────┘

可以调整为:

user.py ─────┐ ▼ domain.py ▲ order.py ────┘

例如把公共模型移出来:

# domain.pyclassUser:pass

随后:

# user.pyfromdomainimportUser
# order.pyfromdomainimportUser

这是大型项目最值得优先考虑的解决方式。


方案二:延迟 import

如果依赖只在运行某个函数时才需要:

defcreate_order():fromuserimportUserreturnUser()

这样导入发生在函数运行阶段,而不是模块初始化阶段。

这种方式简单有效,但不要用它掩盖糟糕的模块边界。


方案三:使用模块引用,而不是立即取属性

相比:

fromuserimportUser

有时:

importuser

更容易处理循环依赖,因为真正访问:

user.User

可以延后发生。


方案四:类型提示使用TYPE_CHECKING

很多循环导入只是为了类型注解。

可以写:

fromtypingimportTYPE_CHECKINGifTYPE_CHECKING:fromuserimportUserdefcreate_order(user:"User"):...

这样静态类型检查器能够看到依赖,但运行时不会真的执行这次 import。


六、importlib、Finder、Loader 分别负责什么?

如果想真正理解 Python import,记住三个角色:

importlib Finder Loader

可以把整个系统想象成物流体系。

你:我要 foo Finder: “我知道 foo 在哪里。” Loader: “我负责把它变成可使用的 Python 模块。” importlib: “我负责组织整个过程。”

1.importlib:Python import 系统的程序化 API

最常用的是:

importimportlib module=importlib.import_module("json")

效果类似:

importjson

真正的价值在于模块名可以动态生成:

module_name="plugins.email"plugin=importlib.import_module(module_name)

这正是插件系统、框架自动发现、配置驱动模块加载的基础。

官方文档也推荐使用importlib.import_module()进行程序化模块导入。(Python documentation)


2. Finder:模块“在哪里”?

Finder 负责回答:

myapp.services.payment到底在哪里?

Python 会遍历:

sys.meta_path

可以看看当前解释器中有哪些 Finder:

importsysforfinderinsys.meta_path:print(finder)

常见角色包括:

BuiltinImporter FrozenImporter PathFinder

Finder 通常通过:

find_spec()

返回:

ModuleSpec

例如:

importimportlib.util spec=importlib.util.find_spec("json")print(spec)print(spec.origin)print(spec.loader)

ModuleSpec可以理解成模块的“装货单”:

模块叫什么? 从哪里来? 谁负责加载? 是不是 package? 子模块应该去哪里找?

Finder 本身通常不负责执行模块代码。它主要负责找到模块,并提供ModuleSpec。(Python documentation)


3. Loader:真正创建和执行模块

Loader 负责:

创建 module 执行模块代码 填充 module.__dict__

现代 Loader 主要关注两个接口:

create_module(spec)exec_module(module)

其中真正执行 Python 模块顶层代码的关键步骤是:

exec_module(module)

所以 import 可以粗略理解为:

spec=finder.find_spec(name)module=importlib.util.module_from_spec(spec)sys.modules[name]=module spec.loader.exec_module(module)

官方importlib文档甚至给出了与此非常接近的程序化导入示例。(Python documentation)


七、怎样利用importlib实现插件动态加载?

假设我们正在开发一个数据处理平台。

希望用户通过配置:

PLUGINS=["plugins.csv_plugin","plugins.json_plugin",]

动态加载插件。

项目结构:

project/ ├── main.py └── plugins/ ├── __init__.py ├── csv_plugin.py └── json_plugin.py

每个插件约定实现:

defrun(data):...

例如:

# plugins/csv_plugin.pydefrun(data):print("CSV plugin:",data)

然后:

importimportlibdefload_plugin(module_name:str):module=importlib.import_module(module_name)ifnothasattr(module,"run"):raiseTypeError(f"{module_name}没有实现 run()")returnmodule plugins=[load_plugin("plugins.csv_plugin"),load_plugin("plugins.json_plugin"),]forplugininplugins:plugin.run({"name":"Python"})

这样配置决定加载什么,而不是业务代码写死:

importplugins.csv_pluginimportplugins.json_plugin

更工程化:定义插件协议

可以配合Protocol

fromtypingimportProtocol,AnyclassPlugin(Protocol):defrun(self,data:Any)->Any:...

然后插件只需要满足协议。

真实工程中,建议继续增加:

插件名称 版本 能力声明 初始化 生命周期 异常隔离 依赖版本检查

例如:

classBasePlugin:name="unknown"version="1.0"definitialize(self):passdefrun(self,data):raiseNotImplementedErrordefshutdown(self):pass

八、更成熟的插件系统:Entry Points

如果插件来自不同的 pip 包,那么手动维护:

"plugins.foo""plugins.bar"

很快就会变得麻烦。

更成熟的方式是使用 Python 包生态中的Entry Points

宿主程序:

fromimportlib.metadataimportentry_pointsdefload_plugins():plugins=entry_points(group="myapp.plugins")forentryinplugins:plugin=entry.load()yieldentry.name,plugin

第三方包安装后可以声明:

[project.entry-points."myapp.plugins"] csv = "my_csv_plugin:CSVPlugin" excel = "my_excel_plugin:ExcelPlugin"

然后:

pipinstallmy-csv-plugin

宿主系统就能够发现它。

Python 当前importlib.metadataAPI 支持按group查询 entry points,并通过:

entry.load()

加载对应对象。(Python documentation)

这类架构很适合:

CLI 扩展 工作流平台 数据处理系统 测试框架 IDE 插件 业务规则引擎 模型适配器

不过要记住:

动态加载 Python 插件,本质上就是执行 Python 代码。

不要从不可信配置中直接执行:

importlib.import_module(user_input)

生产环境应该建立插件白名单、包来源控制、版本约束和权限边界。


九、真正的生产问题:服务启动 8 秒,5 秒耗在 import,怎么诊断?

现在来到最有工程价值的部分。

假设:

Python Web Service 冷启动:8.0 秒 其中: import:5.0 秒 其他初始化:3.0 秒

不要第一反应就是:

“Python import 太慢。”

import 本身通常并不是根因。

真正昂贵的往往是 import 触发的大量模块级代码。

例如:

# model.pyimporttorch model=load_big_model()

或者:

# database.pyclient=connect_database()

甚至:

# config.pyCONFIG=requests.get("https://config-server/config").json()

这些操作全发生在:

importmodelimportdatabaseimportconfig

期间。

所以你看到的“import 5 秒”,很可能实际上是:

import ├── 文件读取 ├── C 扩展加载 ├── 网络访问 ├── 数据库连接 ├── 模型初始化 ├── 配置解析 └── 数千个级联 import

十、第一步:使用-X importtime

CPython 自带一个非常有价值的分析功能:

python-Ximporttime-mmy_service

如果只是想隔离某个入口模块:

python-Ximporttime-c"import my_service"

你会看到类似:

import time: self [us] | cumulative | imported package import time: 200 | 200 | foo import time: 800 | 1000 | bar import time: 850000 | 851000 | pandas

这里需要区分两个指标。

self

模块自身 import 所花的时间。

cumulative

包含其子依赖之后的累计时间。

假设:

A └── B └── C

如果:

A cumulative = 2 秒 A self = 10 毫秒

说明:

A 本身几乎不慢,是它拉进来的依赖慢。

这是 import 性能分析里最重要的判断之一。

Python 官方命令行文档确认,-X importtime会输出每次 import 的自身耗时和累计耗时;在 Python 3.14 中还增加了-X importtime=2,用于显示已经缓存的模块导入。(Python documentation)

也可以:

PYTHONPROFILEIMPORTTIME=1python-mmy_service

十一、第二步:把 import 耗时排序

把日志保存下来:

python-Ximporttime-mmy_service2>import.log

然后重点寻找:

self time 高 cumulative time 高

的模块。

例如最终得到:

模块selfcumulative
app.api20 ms4300 ms
pandas900 ms1500 ms
reports50 ms1800 ms
ml.pipeline1200 ms1300 ms
config700 ms720 ms

一眼就能发现:

app.api

不是根因。

真正值得检查的是:

pandas ml.pipeline config

特别是:

self time 很高

的内部模块。


十二、第三步:检查所有模块级副作用

看到慢模块之后,直接打开代码。

重点搜索:

client=...engine=...model=...df=...requests.get(...)open(...)load(...)connect(...)scan(...)

危险写法:

# service.pyMODEL=load_model("model.bin")

这意味着:

importservice

就必须加载模型。

可以改成:

_model=Nonedefget_model():global_modelif_modelisNone:_model=load_model("model.bin")return_model

这样:

importservice

只负责定义函数。

真正需要模型时才加载:

model=get_model()

这就是典型的 lazy initialization。


十三、一个真实感很强的优化案例

假设服务有一个报表接口:

# reports.pyimportpandasaspddefbuild_report(data):df=pd.DataFrame(data)returndf.describe()

而整个 Web 服务启动时:

fromreportsimportbuild_report

即使 99% 的请求都不会访问报表接口,服务还是必须在启动阶段加载 Pandas。

如果 profiling 显示:

pandas import:1.2 秒

可以调整为:

defbuild_report(data):importpandasaspd df=pd.DataFrame(data)returndf.describe()

冷启动可能立刻减少约 1 秒。

当然代价是:

第一次访问报表接口时,会承担 Pandas 的首次导入延迟。

所以真正的工程问题不是:

要不要 lazy import?

而是:

这 1 秒延迟应该发生在什么时候?

例如 Serverless 应用非常在意冷启动,那么延迟加载很有价值。

而一个长期运行、首个请求 SLA 很严格的 API 服务,则可能更适合:

进程启动 ↓ 核心模块初始化 ↓ 服务 Ready ↓ 后台预热可选依赖

性能优化永远是在移动成本,而不是让成本凭空消失。


十四、__init__.py可能是隐藏的性能杀手

很多项目喜欢在:

package/__init__.py

里写:

from.userimport*from.orderimport*from.reportimport*from.machine_learningimport*

于是:

importpackage

本来只想得到一个 package,却瞬间加载:

User Order Pandas NumPy Torch 数据库 SDK 云平台 SDK ……

大型项目建议保持:

__init__.py

尽量薄。

尤其不要为了所谓“优雅 API”就把整个依赖树全部提前导入。


十五、5 秒 import,我会按这个顺序排查

遇到真实生产问题,我通常不会直接修改代码,而是按照下面的路径推进:

1. 精确测量启动总时间 ↓ 2. -X importtime 找慢模块 ↓ 3. 区分 self / cumulative ↓ 4. 找出 import dependency tree ↓ 5. 检查模块级副作用 ↓ 6. 找重型可选依赖 ↓ 7. 检查 __init__.py ↓ 8. 延迟非关键 import / 初始化 ↓ 9. 重构依赖边界 ↓ 10. 再次 benchmark

同时注意区分:

冷文件系统 热文件系统 开发机器 Docker 容器 首次启动 重复启动 本地磁盘 网络挂载 Python 版本 依赖版本

否则你很可能优化了一个并不存在于生产环境的问题。


十六、几个非常实用的 import 最佳实践

1. 模块顶层尽量只做“声明”

理想:

DEFAULT_TIMEOUT=10classClient:...defcreate_client():...

谨慎:

CLIENT=connect_remote_service()

危险:

DATA=requests.get(REMOTE_URL).json()

2. import 阶段不要访问网络

不要:

CONFIG=fetch_remote_config()

更合理的是:

defload_config():returnfetch_remote_config()

然后在明确的启动生命周期中调用。


3. 保持依赖方向单向

优秀的工程结构更接近:

API ↓ Service ↓ Domain ↓ Infrastructure

而不是:

A → B → C ↑ ↓ └───────┘

大量 circular import 往往不是 Python 问题,而是架构边界正在发出警报。


4. 不要滥用sys.path.append()

很多项目中出现:

importsys sys.path.append("../../")

这通常是在用运行时路径修改掩盖包结构问题。

更好的办法是:

规范 package 使用 pyproject.toml 正确安装项目 使用绝对/相对导入

5. 不要用删除sys.modules实现热更新

这可能让同一个模块在进程里存在多个实例:

old module object new module object

进一步造成:

类型判断异常 单例失效 全局状态分裂 旧对象继续引用旧类

生产热更新应该使用成熟的进程重启、worker reload 或部署机制。


十七、一个值得记住的 Import 心智模型

以后再看到:

importfoo

不要只把它想成:

读取 foo.py

而应该想到:

import foo │ ▼ sys.modules 有吗? / \ 有 没有 │ │ ▼ ▼ 返回缓存 Finder 查找 │ ▼ ModuleSpec │ ▼ 创建 module │ ▼ 放入 sys.modules │ ▼ Loader 执行代码 │ ▼ 初始化完成

只要这个模型真正建立起来,很多曾经看起来很“玄学”的 Python 问题都会突然变得清晰:

为什么第二次 import 不执行? 为什么循环导入不是无限递归? 为什么会出现 partially initialized module? 为什么删除 sys.modules 很危险? 为什么 importlib 可以动态加载插件? 为什么 import 可能让服务启动变慢?

它们其实都是同一套机制在不同场景下的表现。


十八、最后:真正值得优化的往往不是 import,而是模块边界

Python 的 import system 表面上是一个语言基础设施,深入进去以后,却会一路碰到:

模块设计 依赖管理 架构分层 插件化 启动性能 可测试性 部署方式

这也是 Python 有意思的地方。

一个最普通的:

importfoo

背后已经是一套完整的运行时模块系统。

如果你的服务出现:

启动 8 秒 其中 import 5 秒

不要急着寻找所谓的“加速 import 黑科技”。

先问三个问题:

  1. 到底是哪几个模块慢?
  2. 慢的是模块搜索,还是模块顶层代码执行?
  3. 这些工作真的必须发生在 import 阶段吗?

很多时候,仅仅把:

model=load_model()database=connect()config=fetch_config()

从模块顶层移到明确的应用生命周期中,服务架构就会同时获得:

更快启动 更容易测试 更少副作用 更清晰依赖 更高可维护性

性能问题最终常常会把我们带回软件工程最基本的一句话:

让模块负责定义能力,让应用生命周期负责启动能力。

这比单纯追求“少 import 几个包”重要得多。


参考资料

Python 官方《导入系统》详细解释了sys.modules、Finder、Loader、ModuleSpec 以及模块加载过程。Python 官方:The import system

程序化导入、自定义 Finder / Loader、find_spec()与动态加载相关接口可参考importlib官方文档。Python 官方:importlib

插件生态中的 Entry Points 可参考importlib.metadata。Python 官方:importlib.metadata

启动耗时分析中的-X importtimePYTHONPROFILEIMPORTTIME可参考 Python 命令行官方文档。Python 官方:命令行与环境变量


留给读者的两个问题

你是否遇到过这样一种循环导入:明明把某个import移进函数里就“修好了”,但半年之后项目里出现了更多循环依赖?如果重新设计,你会怎样划分模块边界?

以及,如果一个 Python 服务的启动时间从:

2 秒 → 5 秒 → 10 秒

一路增长,你认为应该从什么时候开始把import time正式纳入 CI 性能指标?

欢迎把你的案例、踩坑经历和优化方案分享出来。理解 Python 的最好方式,从来不只是记住语法,而是不断追问:

“这一行代码,在解释器内部究竟发生了什么?”

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询