Python import机制:模块查找路径、import的执行流程、模块缓存机制

聊到脚本启动的延时加载,咱们得先搞清楚一个基础问题——Python 到底是怎么找到并加载一个模块的?

我刚开始用 Python 那会儿,遇到过一件挺尴尬的事。自己写了个 utils.py,结果怎么 import 都报错。折腾了半天才发现,是文件名跟标准库重名了。嗯,从那以后我就认真研究了一下 import 的底层机制。

说白了,import 就干三件事:找文件、读文件、缓存结果。咱们一个一个来看。

模块查找路径:sys.path 的秘密

当你写下 import requests,Python 到底去哪儿找这个模块?

答案藏在 sys.path 里。这是一个列表,里面存了一堆路径。Python 会按顺序挨个翻一遍。

import sys
print(sys.path)
# 输出大概长这样:
# ['/当前项目目录',
#  '/usr/lib/python3.10',
#  '/usr/lib/python3.10/lib-dynload',
#  '/usr/local/lib/python3.10/dist-packages',
#  ...]

我个人习惯把这个列表记成三部分:

来源 说明 典型路径
当前脚本所在目录 自动添加,排在最前面 /home/user/my_project
Python 标准库路径 解释器自带的模块 /usr/lib/python3.10
第三方包安装路径 pip install 放的地方 site-packages

你想想看,如果两个路径下有同名模块,谁排在前面谁就被加载。这就是为什么我建议项目里别用 string.py、json.py 这种名字——标准库里已经有了,你的文件会被忽略掉。

注意: 我曾经在项目里建了个 logging.py,结果整个项目的日志全丢了。因为 Python 先找到了我的文件,标准库的 logging 根本没加载。排查了整整一下午。

import 的执行流程:一步步拆解

好,路径找到了,接下来 Python 怎么把模块装进来?

我习惯把 import 拆成 5 步:

  1. 检查缓存 —— 看看 sys.modules 里有没有
  2. 查找模块 —— 遍历 sys.path 找文件
  3. 加载模块 —— 编译 + 执行代码
  4. 缓存模块 —— 存到 sys.modules
  5. 绑定名称 —— 把模块赋值给变量

举个例子,你写 import os:

# 第一步:检查 sys.modules
if 'os' in sys.modules:
    module = sys.modules['os']
else:
    # 第二步:在 sys.path 里找 os.py
    # 第三步:找到后编译执行
    # 第四步:存入 sys.modules
    # 第五步:绑定到当前命名空间
    import os  # 实际执行的就是这五步

这里有个关键点——模块里的代码只执行一次。第二次 import 的时候,直接从缓存拿了,不会再跑一遍。

核心结论: import 的开销主要发生在第一次。后续的 import 几乎没成本,因为走的是缓存。

模块缓存机制:sys.modules 的妙用

sys.modules 是个字典,key 是模块名,value 是模块对象。所有已经加载过的模块都在这儿。

import sys
print(len(sys.modules))  # 看看已经加载了多少模块
# 输出:大概 50~80 个(刚启动时)

我做过一个实验:

import time

# 第一次 import
start = time.time()
import json
print(f"第一次:{time.time() - start:.6f} 秒")

# 第二次 import
start = time.time()
import json
print(f"第二次:{time.time() - start:.6f} 秒")

# 输出:
# 第一次:0.002345 秒
# 第二次:0.000001 秒

看到了吧?差了三个数量级。这就是缓存的威力。

那这个机制跟延时加载有什么关系?关系大了。

你想想看,如果你的脚本一启动就 import 了 50 个模块,每个模块哪怕只花 0.01 秒,加起来就是 0.5 秒。用户等得心焦。

而延时加载的思路就是——用的时候再 import,用完就扔缓存里。反正第二次调用几乎不花时间。

小技巧: 你可以用 sys.modules.keys() 看看当前加载了哪些模块。我经常用这招排查「某个模块是不是被提前加载了」。

一张图看懂 import 机制

下面这张图是我自己画的,把整个流程串起来了:

Python import 执行流程 import xxx 在 sys.modules 中? 是 直接返回缓存 否 遍历 sys.path 查找模块 编译 + 执行模块代码 存入 sys.modules 绑定名称,完成

这张图我画了好几次才满意。核心就一句话:缓存命中就走快车道,没命中才走完整流程。

实际项目中的应用

理解了 import 机制,咱们就能做很多优化了。

比如我做过一个 CLI 工具,启动时只 import 了 3 个核心模块。其他 20 多个模块,都在具体函数里按需 import:

# 启动时只加载必要的
import sys
import argparse
import json

def handle_network():
    # 用的时候再加载
    import requests
    import socket
    # ... 处理逻辑

def handle_database():
    # 同理
    import pymongo
    from sqlalchemy import create_engine
    # ... 处理逻辑

这样启动时间从 1.2 秒降到了 0.3 秒。用户反馈「快多了」。

我的建议: 写库或者写工具时,把重型模块(比如 pandas、numpy、requests)放到函数内部 import。启动时只 import 轻量模块。这是最直接的延时加载手段。

嗯,import 机制就聊到这儿。记住三个关键词:sys.path 决定去哪找,sys.modules 决定是否缓存,第一次加载最耗时。后面讲延时加载方案时,这些知识会反复用到。

可可精品资料库