大家好,我是正在实战各种 AI 项目的程序员晚枫。

import 语句背后发生了什么?模块缓存、导入钩子、相对导入...这一讲,揭开 Python 导入系统的全部秘密。


📖 开篇:import 比你想象的复杂

1
2
3
4
import os
from collections import defaultdict
import numpy as np
from . import utils # 相对导入

这些 import 语句背后,Python 做了大量工作:

  1. 查找模块(sys.path)
  2. 加载模块代码
  3. 缓存模块对象(sys.modules)
  4. 绑定到命名空间

🔍 完整导入流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os

Step 1: 检查 sys.modules
sys.modules 中已有 os?
↓ 是
直接返回缓存的模块对象
↓ 否

Step 2: 查找模块(Finder 链)
sys.meta_path 中的每个 Finder 尝试 find_module()

Step 3: 加载模块(Loader)
Finder 返回 Loader,调用 load_module()

Step 4: 执行模块代码
创建模块对象 -> 执行模块代码 -> 设置 __name__ 等属性

Step 5: 缓存
加入 sys.modules

Step 6: 绑定
在当前命名空间中创建变量 os 指向模块对象

sys.modules 详解

1
2
3
4
5
6
7
8
import sys

# sys.modules 是所有已导入模块的缓存字典
print(len(sys.modules)) # 几百个!
print('os' in sys.modules) # True(import os 之后)

# 手动操作缓存(危险!)
# del sys.modules['os'] # 下次 import 会重新加载

sys.path(模块查找路径)

1
2
3
4
5
6
7
8
9
import sys
for i, path in enumerate(sys.path):
print(f'{i}: {path}')

# sys.path 的顺序:
# 0: '' (当前目录)
# 1: 标准库路径
# 2: site-packages (第三方库)
# 3: ~/.local/lib (用户包)

🎯 Finder 与 Loader

元路径查找器(Meta Path Finder)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sys

# sys.meta_path 包含所有 Finder
for finder in sys.meta_path:
print(finder)

# 自定义 Finder:拦截模块导入
class DebugFinder:
def find_module(self, fullname, path=None):
print(f'尝试导入: {fullname}')
return None # None 表示没找到,继续下一个 Finder

sys.meta_path.insert(0, DebugFinder())

import json # 会打印:尝试导入: json

自定义 Loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys
import importlib.abc
import importlib.machinery

class MyLoader(importlib.abc.Loader):
def create_module(self, spec):
return None

def exec_module(self, module):
# 执行模块代码,填充 module 的属性
module.my_var = 'Hello from MyLoader!'
module.my_func = lambda: 'func called'

# 注册 Loader
spec = importlib.machinery.ModuleSpec(
'my_module', MyLoader(), origin='custom'
)
sys.modules['my_module'] = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sys.modules['my_module'])

import my_module
print(my_module.my_var) # Hello from MyLoader!

📦 包(Package)的导入

init.py

1
2
3
4
5
6
7
8
9
10
# mypackage/__init__.py
print('mypackage 正在初始化...')

from . import utils
from .core import process

__all__ = ['utils', 'process', 'init']

def init():
print('mypackage initialized')
1
2
3
4
import mypackage
# 输出: mypackage 正在初始化...
# mypackage.utils 和 mypackage.process 已导入
mypackage.init()

相对导入

1
2
3
4
# mypackage/core/__init__.py
from . import helpers # 相对导入,从当前包导入
# from .. import utils # 从上级包导入
# from ..other import something # 从上上级包导入

main.py(可执行包)

1
2
# mypackage/__main__.py
python -m mypackage # 执行 __main__.py
1
2
3
4
5
# mypackage/__main__.py
from .core import main

if __name__ == '__main__':
main()

🔄 重新加载模块

1
2
3
4
5
6
7
8
9
10
11
12
13
import importlib

# 普通 import 不会重新加载(已在 sys.modules)
import json
import json # 不重新加载!

# 强制重新加载
importlib.reload(json) # 重新执行模块代码

# 典型场景:开发时重新加载修改后的模块
import mymodule
# ... 修改 mymodule.py ...
importlib.reload(mymodule)

⚠️ 常见陷阱

陷阱1:循环导入

1
2
3
4
5
6
7
8
9
10
11
# a.py
from b import B # b 还没加载完成!
class A:
pass

# b.py
from a import A # a 还没加载完成!
class B:
pass

import a # RuntimeError 或 AttributeError!

解决:延迟导入(在函数内部 import)、重构模块结构。

陷阱2:导入顺序依赖

1
2
# __init__.py 依赖某个模块先被导入
# 但 import 顺序不确定 -> 容易出问题

💡 本节作业

  1. 打印 sys.path 并解释每条路径的作用
  2. 写一个 DebugFinder 拦截所有 import 请求
  3. 创建自己的包,包含 init.py、utils.py,验证导入行为

🎯 本讲总结

导入流程:检查缓存 -> Finder 查找 -> Loader 执行 -> 缓存模块 -> 绑定命名空间。

sys.modules:模块缓存字典,所有已导入模块都在这里。

sys.path:模块查找路径列表,按顺序搜索。

Finder/Loader:元路径查找器和加载器,可以自定义导入行为。

包的结构init.py、main.py、相对导入。


📚 推荐教材

《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》


🔗 课程导航

上一讲:线程与并发 | 下一讲:C 扩展编程


💬 联系我

平台账号/链接
微信扫码加好友
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!