Python import 到底做了什么?从 sys.modules、循环导入到插件加载与 5 秒冷启动诊断
很多 Python 开发者第一次接触 import 时,会自然地把它理解为一句很简单的话:
“把另一个
.py文件拿过来用。”
这个理解不能说错,但只覆盖了表面。
在真实的 Python 工程里,import 背后牵涉到模块缓存、路径搜索、模块创建、代码执行、命名空间绑定、Finder、Loader、ModuleSpec 以及动态加载机制。一旦项目规模扩大,这些机制会直接影响架构设计、循环依赖、插件系统,甚至服务的冷启动性能。
比如这两行代码:
import foo
import foo
为什么 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 = 42
def hello():
print("hello")
然后创建 main.py:
import foo
import foo
print(foo.VALUE)
执行:
python main.py
结果通常是:
foo.py 正在执行
42
注意:
foo.py 正在执行
只出现了一次。
原因并不是 Python 看到“上一行刚 import 过”,而是 Python 维护着一张非常重要的模块缓存:
sys.modules
第一次:
import foo
完成后,大致会产生这样的状态:
import sys
print(sys.modules["foo"])
输出类似:
<module 'foo' from '/project/foo.py'>
也就是说:
sys.modules
↓
{
"foo": <module foo>
}
第二次执行:
import foo
Python 首先检查:
"foo" in sys.modules
发现模块已经存在,于是直接复用已有模块对象,而不会再次执行 foo.py 的顶层代码。
Python 官方导入系统文档也明确说明:导入搜索首先检查 sys.modules;如果对应模块已经存在,通常直接返回缓存中的模块对象。(Python documentation)
二、sys.modules 到底是什么?
可以把:
sys.modules
理解成 Python 解释器当前进程中的:
模块对象注册表 + 一级 import 缓存。
它本质上是一个字典。
import sys
print(type(sys.modules))
输出:
<class 'dict'>
键是完整模块名:
"json"
"os"
"collections"
"package.submodule"
值是对应的模块对象。
例如:
import json
import sys
print(sys.modules["json"] is json)
结果:
True
甚至可以观察两次 import 得到的对象:
import foo
first = foo
import foo
second = foo
print(first is second)
输出:
True
所以更准确地说:
import foo
并不意味着:
每次都重新读取 foo.py。
而更接近:
if "foo" in sys.modules:
foo = sys.modules["foo"]
else:
# 搜索、创建并执行 foo
...
当然,真实实现复杂得多。
可以手动删除 sys.modules 吗?
技术上可以:
import sys
import foo
del sys.modules["foo"]
import foo
这时 Python 会重新进行一次模块搜索和加载。
但是不要把它当作普通的“重新加载模块”方案。
因为:
old_foo = foo
del sys.modules["foo"]
import foo as new_foo
print(old_foo is new_foo)
通常得到:
False
旧模块对象并不会因为删除 sys.modules 项就自动消失,因为其他地方可能仍然持有它的引用。
如果真的需要重新执行模块,更合适的是:
import importlib
import foo
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] = module
try:
loader.exec_module(module)
except BaseException:
del sys.modules[name]
raise
也就是说,模块的代码还没执行完,它就已经出现在 sys.modules 中了。
这是循环导入问题的核心。(Python documentation)
四、为什么循环导入会出现 partially initialized module?
假设有两个文件。
user.py:
from order import create_order
class User:
pass
order.py:
from user import User
def create_order():
return User()
现在执行:
import user
发生了什么?
第一步:开始加载 user
Python 创建:
module user
然后先登记:
sys.modules["user"] = user
但是此时:
class User:
...
还没有执行。
于是现在的 user 模块可以近似看成:
user.__dict__ = {
"__name__": "user",
...
}
里面暂时没有:
User
第二步:执行到 from order import create_order
Python 开始加载 order.py。
同样:
sys.modules["order"] = order
然后执行:
from user import User
第三步:再次寻找 user
Python一查:
"user" in sys.modules
结果是:
True
于是不会重新执行 user.py。
问题来了。
当前 user 虽然已经存在,但:
class User:
那一行还没运行。
于是:
user.User
不存在。
这就是:
partially initialized module
它不是一个神秘状态。
翻译成人话就是:
模块对象已经创建并注册了,但模块顶层代码还没有执行完成。
五、循环导入怎么解决?
不要看到循环导入就机械地把 import 移到函数里面。真正应该问的是:
为什么模块 A 和模块 B 在架构上互相依赖?
常见解决方案有四种。
方案一:提取共同依赖
原来:
user.py ───► order.py
▲ │
└───────────┘
可以调整为:
user.py ─────┐
▼
domain.py
▲
order.py ────┘
例如把公共模型移出来:
# domain.py
class User:
pass
随后:
# user.py
from domain import User
# order.py
from domain import User
这是大型项目最值得优先考虑的解决方式。
方案二:延迟 import
如果依赖只在运行某个函数时才需要:
def create_order():
from user import User
return User()
这样导入发生在函数运行阶段,而不是模块初始化阶段。
这种方式简单有效,但不要用它掩盖糟糕的模块边界。
方案三:使用模块引用,而不是立即取属性
相比:
from user import User
有时:
import user
更容易处理循环依赖,因为真正访问:
user.User
可以延后发生。
方案四:类型提示使用 TYPE_CHECKING
很多循环导入只是为了类型注解。
可以写:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from user import User
def create_order(user: "User"):
...
这样静态类型检查器能够看到依赖,但运行时不会真的执行这次 import。
六、importlib、Finder、Loader 分别负责什么?
如果想真正理解 Python import,记住三个角色:
importlib
Finder
Loader
可以把整个系统想象成物流体系。
你:我要 foo
Finder:
“我知道 foo 在哪里。”
Loader:
“我负责把它变成可使用的 Python 模块。”
importlib:
“我负责组织整个过程。”
1. importlib:Python import 系统的程序化 API
最常用的是:
import importlib
module = importlib.import_module("json")
效果类似:
import json
真正的价值在于模块名可以动态生成:
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:
import sys
for finder in sys.meta_path:
print(finder)
常见角色包括:
BuiltinImporter
FrozenImporter
PathFinder
Finder 通常通过:
find_spec()
返回:
ModuleSpec
例如:
import importlib.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
每个插件约定实现:
def run(data):
...
例如:
# plugins/csv_plugin.py
def run(data):
print("CSV plugin:", data)
然后:
import importlib
def load_plugin(module_name: str):
module = importlib.import_module(module_name)
if not hasattr(module, "run"):
raise TypeError(
f"{module_name} 没有实现 run()"
)
return module
plugins = [
load_plugin("plugins.csv_plugin"),
load_plugin("plugins.json_plugin"),
]
for plugin in plugins:
plugin.run({"name": "Python"})
这样配置决定加载什么,而不是业务代码写死:
import plugins.csv_plugin
import plugins.json_plugin
更工程化:定义插件协议
可以配合 Protocol:
from typing import Protocol, Any
class Plugin(Protocol):
def run(self, data: Any) -> Any:
...
然后插件只需要满足协议。
真实工程中,建议继续增加:
插件名称
版本
能力声明
初始化
生命周期
异常隔离
依赖版本检查
例如:
class BasePlugin:
name = "unknown"
version = "1.0"
def initialize(self):
pass
def run(self, data):
raise NotImplementedError
def shutdown(self):
pass
八、更成熟的插件系统:Entry Points
如果插件来自不同的 pip 包,那么手动维护:
"plugins.foo"
"plugins.bar"
很快就会变得麻烦。
更成熟的方式是使用 Python 包生态中的 Entry Points。
宿主程序:
from importlib.metadata import entry_points
def load_plugins():
plugins = entry_points(group="myapp.plugins")
for entry in plugins:
plugin = entry.load()
yield entry.name, plugin
第三方包安装后可以声明:
[project.entry-points."myapp.plugins"]
csv = "my_csv_plugin:CSVPlugin"
excel = "my_excel_plugin:ExcelPlugin"
然后:
pip install my-csv-plugin
宿主系统就能够发现它。
Python 当前 importlib.metadata API 支持按 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.py
import torch
model = load_big_model()
或者:
# database.py
client = connect_database()
甚至:
# config.py
CONFIG = requests.get(
"https://config-server/config"
).json()
这些操作全发生在:
import model
import database
import config
期间。
所以你看到的“import 5 秒”,很可能实际上是:
import
├── 文件读取
├── C 扩展加载
├── 网络访问
├── 数据库连接
├── 模型初始化
├── 配置解析
└── 数千个级联 import
十、第一步:使用 -X importtime
CPython 自带一个非常有价值的分析功能:
python -X importtime -m my_service
如果只是想隔离某个入口模块:
python -X importtime -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=1 python -m my_service
十一、第二步:把 import 耗时排序
把日志保存下来:
python -X importtime -m my_service 2> import.log
然后重点寻找:
self time 高
cumulative time 高
的模块。
例如最终得到:
| 模块 | self | cumulative |
|---|---|---|
app.api | 20 ms | 4300 ms |
pandas | 900 ms | 1500 ms |
reports | 50 ms | 1800 ms |
ml.pipeline | 1200 ms | 1300 ms |
config | 700 ms | 720 ms |
一眼就能发现:
app.api
不是根因。
真正值得检查的是:
pandas
ml.pipeline
config
特别是:
self time 很高
的内部模块。
十二、第三步:检查所有模块级副作用
看到慢模块之后,直接打开代码。
重点搜索:
client = ...
engine = ...
model = ...
df = ...
requests.get(...)
open(...)
load(...)
connect(...)
scan(...)
危险写法:
# service.py
MODEL = load_model("model.bin")
这意味着:
import service
就必须加载模型。
可以改成:
_model = None
def get_model():
global _model
if _model is None:
_model = load_model("model.bin")
return _model
这样:
import service
只负责定义函数。
真正需要模型时才加载:
model = get_model()
这就是典型的 lazy initialization。
十三、一个真实感很强的优化案例
假设服务有一个报表接口:
# reports.py
import pandas as pd
def build_report(data):
df = pd.DataFrame(data)
return df.describe()
而整个 Web 服务启动时:
from reports import build_report
即使 99% 的请求都不会访问报表接口,服务还是必须在启动阶段加载 Pandas。
如果 profiling 显示:
pandas import:1.2 秒
可以调整为:
def build_report(data):
import pandas as pd
df = pd.DataFrame(data)
return df.describe()
冷启动可能立刻减少约 1 秒。
当然代价是:
第一次访问报表接口时,会承担 Pandas 的首次导入延迟。
所以真正的工程问题不是:
要不要 lazy import?
而是:
这 1 秒延迟应该发生在什么时候?
例如 Serverless 应用非常在意冷启动,那么延迟加载很有价值。
而一个长期运行、首个请求 SLA 很严格的 API 服务,则可能更适合:
进程启动
↓
核心模块初始化
↓
服务 Ready
↓
后台预热可选依赖
性能优化永远是在移动成本,而不是让成本凭空消失。
十四、__init__.py 可能是隐藏的性能杀手
很多项目喜欢在:
package/__init__.py
里写:
from .user import *
from .order import *
from .report import *
from .machine_learning import *
于是:
import package
本来只想得到一个 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 = 10
class Client:
...
def create_client():
...
谨慎:
CLIENT = connect_remote_service()
危险:
DATA = requests.get(REMOTE_URL).json()
2. import 阶段不要访问网络
不要:
CONFIG = fetch_remote_config()
更合理的是:
def load_config():
return fetch_remote_config()
然后在明确的启动生命周期中调用。
3. 保持依赖方向单向
优秀的工程结构更接近:
API
↓
Service
↓
Domain
↓
Infrastructure
而不是:
A → B → C
↑ ↓
└───────┘
大量 circular import 往往不是 Python 问题,而是架构边界正在发出警报。
4. 不要滥用 sys.path.append()
很多项目中出现:
import sys
sys.path.append("../../")
这通常是在用运行时路径修改掩盖包结构问题。
更好的办法是:
规范 package
使用 pyproject.toml
正确安装项目
使用绝对/相对导入
5. 不要用删除 sys.modules 实现热更新
这可能让同一个模块在进程里存在多个实例:
old module object
new module object
进一步造成:
类型判断异常
单例失效
全局状态分裂
旧对象继续引用旧类
生产热更新应该使用成熟的进程重启、worker reload 或部署机制。
十七、一个值得记住的 Import 心智模型
以后再看到:
import foo
不要只把它想成:
读取 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 有意思的地方。
一个最普通的:
import foo
背后已经是一套完整的运行时模块系统。
如果你的服务出现:
启动 8 秒
其中 import 5 秒
不要急着寻找所谓的“加速 import 黑科技”。
先问三个问题:
- 到底是哪几个模块慢?
- 慢的是模块搜索,还是模块顶层代码执行?
- 这些工作真的必须发生在 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 importtime、PYTHONPROFILEIMPORTTIME 可参考 Python 命令行官方文档。Python 官方:命令行与环境变量
留给读者的两个问题
你是否遇到过这样一种循环导入:明明把某个 import 移进函数里就“修好了”,但半年之后项目里出现了更多循环依赖?如果重新设计,你会怎样划分模块边界?
以及,如果一个 Python 服务的启动时间从:
2 秒 → 5 秒 → 10 秒
一路增长,你认为应该从什么时候开始把 import time 正式纳入 CI 性能指标?
欢迎把你的案例、踩坑经历和优化方案分享出来。理解 Python 的最好方式,从来不只是记住语法,而是不断追问:
“这一行代码,在解释器内部究竟发生了什么?”

4071

被折叠的 条评论
为什么被折叠?



