sys.modules是python解释器维护的模块缓存字典,键为模块名、值为模块对象;import时优先查此字典,命中即返回,否则才加载,故可预先注入自定义对象实现全局拦截。

sys.modules 是什么,为什么能用来做模块拦截
sys.modules 是 Python 解释器维护的一个字典,键是已导入模块的名称(字符串),值是对应的模块对象。每次执行 import xxx 时,解释器会先查 sys.modules;命中就直接返回,不命中才真正加载模块。这意味着,只要提前在 sys.modules 中塞入一个自定义对象,后续所有同名 import 都会拿到它——这正是“全局拦截”的底层依据。
注意:这不是装饰器或元类那种运行时逻辑劫持,而是利用 import 机制本身的缓存行为,属于轻量、低侵入的控制点。
如何用 sys.modules 替换模块实现单例语义
核心思路是:在首次导入真实模块后,将其包装为单例对象(例如加锁、限制实例化、统一配置等),再把它塞进 sys.modules,覆盖原始模块引用。
常见做法是写一个代理模块(比如 my_singleton.py),并在其中完成替换:
# my_singleton.py
import sys
import importlib
<h1>先导入真实模块(假设是 requests)</h1><p>_real_requests = importlib.import_module('requests')</p><p>class SingletonRequests:
_instance = None
def <strong>new</strong>(cls):
if cls._instance is None:
cls._instance = super().<strong>new</strong>(cls)</p><h1>可在此处初始化共享状态,如 session 复用</h1><pre class="brush:python;toolbar:false;"> cls._instance.session = _real_requests.Session()
return cls._instance构造一个模块级实例
_requests_instance = SingletonRequests()
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
将该实例伪装成模块,暴露常用属性
_requests_instance.name = 'requests' _requests_instance.dict.update(_real_requests.dict) _requests_instance.get = lambda *a, *kw: _requests_instance.session.get(a, **kw)
... 其他方法按需代理
关键:注入 sys.modules,覆盖原模块
sys.modules['requests'] = _requests_instance
之后任何地方 import requests,拿到的都是这个单例实例,而非原始 requests 模块。
- 必须在其他模块 import requests 之前执行这段代码,否则缓存已存在,替换无效
- 不能只替换函数(如
sys.modules['requests'].get),因为 import 语句绑定的是整个模块对象 - 若被代理模块有 C 扩展或动态生成的属性(如
requests.__version__),需显式同步,否则可能丢失
拦截失败的典型现象和排查方向
最常遇到的现象是:明明改了 sys.modules['xxx'],但 import xxx 还是拿到原模块。
原因通常有以下几种:
-
import xxx已在你的拦截代码之前被执行(比如在__init__.py或启动脚本顶部)——检查导入顺序,把拦截逻辑放在最前 - 模块名不匹配:
import pkg.sub查的是'pkg.sub',不是'pkg';子模块需单独拦截 - 使用了相对导入或
from xxx import yyy,而你只替换了xxx模块本身,yyy仍来自原模块命名空间 - 某些框架(如 pytest、IPython)会预加载模块,导致你的替换被跳过——可在
sitecustomize.py或usercustomize.py中提前注入
与 __import__ 或 importlib.import_module 的关系
<strong>import</strong> 和 importlib.import_module 最终都会查 sys.modules,所以你的拦截对它们同样生效。但要注意:
- 手动调用
importlib.reload()会绕过sys.modules缓存,重新加载源码,从而破坏单例——应避免 reload 被拦截模块 - 如果模块内部又 import 自身(比如循环引用场景),可能触发重复初始化,需在代理类中加保护(如
if _instance is not None: return _instance) -
from xxx import yyy本质是先 import xxx,再取属性;只要 xxx 已被替换且正确暴露 yyy,就能正常工作
这种拦截方式不改变语法,也不依赖 AST 改写,但要求你清楚模块加载时机和命名空间边界。稍不留神,就会出现“看起来替换了,实际没生效”的静默失败。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










