_import_ 是python的动态导入函数
我们看 PEP302 这个提案。PEP302的主要内容摘要
提供 import hook 函数以替代传统的 _import_ ,以便更好地定制import 的过程,对导入过程实现更精细的控制。
对于 Python的 Import机制,大致可以简化成几个过程。
名字规范化
处理相对导入(level)并把目标变成绝对模块名 fullname。检查缓存(sys.modules)
如果 fullname 已在 sys.modules 中,直接返回该模块对象(避免重复加载)。查找模块(Finder)
Python 遍历 sys.meta_path 中的查找器(meta path finders),调用每个 finder 的 find_spec(fullname, path, target)(现代)或旧的 find_module。
如果 meta-path 没找到,再根据包的 path 使用 sys.path_hooks 与 sys.path_importer_cache 去查找(这用于 import pkg.submod 时按包路径查找)。获得模块规范(ModuleSpec)
Finder 返回一个 ModuleSpec(包含 loader、origin、submodule_search_locations 等信息)。使用 Loader 创建并执行模块
Loader 的职责是创建模块对象(create_module(spec),可以返回 None 表示使用默认创建器)并执行模块代码(exec_module(module))来填充模块命名空间。
在旧接口里对应的是 load_module(fullname)。模块写入 sys.modules
在加载过程中模块会被放入 sys.modules,以便循环引用时能找到(通常在 exec 前或 exec 期间就已放入,细节由实现决定)。返回模块对象
import 表达式最终返回模块对象或绑定到目标名。
对于一般的 pip安装在本地的模块来说,上面的步骤大致可以理解为,根据名称从磁盘目录加载模块并规范为一个module,放在内存中。
但是很常见的误解开始在这个地方显现
import机制不仅仅是仅能加载操作系统本地路径的查找器,它还可以加载网络模块,zip文件,以及数据库,甚至是内存数据,或者动态生成
一个演示加载的代码示例
# demo_import_steps.py
import sys
import importlib
import importlib.util
name = "math" # 你可以换成任意模块
print("在 import 前 sys.modules 中有吗:", name in sys.modules)
# 1. 使用 importlib.util.find_spec 查看模块的 spec(查找阶段)
spec = importlib.util.find_spec(name)
print("spec:", spec) # None 表示找不到
if spec:
print("origin:", spec.origin)
print("loader:", spec.loader)
# 2. 如果未在 sys.modules,演示如何用 spec 手动创建模块并执行
if name not in sys.modules and spec and spec.loader:
mod = importlib.util.module_from_spec(spec)
# 将模块放入 sys.modules(导入系统通常在 exec 前或中间做这步)
sys.modules[name] = mod
# 执行模块代码(loader 负责)
spec.loader.exec_module(mod)
print("现在 sys.modules 有了吗:", name in sys.modules)
# 3. 标准 import(等效)
import math
print("math.pi =", math.pi)
输出
在 import 前 sys.modules 中有吗: False
spec: ModuleSpec(name='math', loader=<_frozen_importlib_external.ExtensionFileLoader object at 0x7fba1013a970>, origin='/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload/math.cpython-38-darwin.so')
origin: /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload/math.cpython-38-darwin.so
loader: <_frozen_importlib_external.ExtensionFileLoader object at 0x7fba1013a970>
现在 sys.modules 有了吗: True
math.pi = 3.141592653589793
上面这个导入,用了比较现代的 find_spec 这些钩子函数。
回过头来看 PEP302 对于import的改善动机就不难理解了。
导入 import 的旧的范式,如果涉及到重写这个过程,做法就是重新定义实现 _import_ 函数
这样的话,我们必须小心翼翼,以免破坏原有的导入机制。
一旦开始重新,我们要搞清楚整个导入过程,然后再仔细加入我们自定义的东西;
另外,即使对于 sys.modules 中已经存在的模块,也会调用此方法,这几乎永远不是你想要的结果,除非你在编写某种监控工具。
PEP302 定义的两个Protocol: finder 和loader
finder只有一个方法 find_module(fullname, path=None)
loader 也只有一个方法 loader.load_module(fullname) load_module
(PEP451增加了create_module和exec_module)
PEP中示例的一个 mini版本的 load_module
# Consider using importlib.util.module_for_loader() to handle
# most of these details for you.
def load_module(self, fullname):
code = self.get_code(fullname)
ispkg = self.is_package(fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<%s>" % self.__class__.__name__
mod.__loader__ = self
if ispkg:
mod.__path__ = []
mod.__package__ = fullname
else:
mod.__package__ = fullname.rpartition('.')[0]
exec(code, mod.__dict__)
return mod
三类 hook的作用
| Hook | 在哪里注册 | 作用 |
|---|---|---|
| Meta Path Finder | sys.meta_path |
找模块入口(全局) |
| Path Entry Finder | sys.path_hooks |
针对每个路径处理(目录/zip/网络) |
| Loader | Finder 返回 loader | 加载模块(执行代码、构造模块对象) |
import something
└── sys.meta_path(Meta Finders)
└── 大部分模块交给 PathFinder
PathFinder
├── sys.path 遍历每个路径
├── 为每个 path 创建 PathEntryFinder(由 sys.path_hooks 决定)
└── PathEntryFinder.find_spec
└── 返回 ModuleSpec(含 Loader)
└── Loader.exec_module() 执行模块代码
示例:为特殊前缀路径注册 Finder
import sys
class VirtualPathFinder:
def __init__(self, path):
self.path = path
def find_spec(self, fullname, target=None):
if fullname == "hello":
from importlib.util import spec_from_loader
return spec_from_loader("hello", loader=None)
return None
def hook(path):
if path.startswith("virtual:"):
return VirtualPathFinder(path)
raise ImportError
sys.path_hooks.insert(0, hook)
sys.path.insert(0, "virtual:/memory")
自定义 Loader 加载字符串为模块
import sys, types, importlib.util
class StringLoader:
def create_module(self, spec):
return None # 使用默认 module 创建逻辑
def exec_module(self, module):
code = "x = 123\ny = 'hello'"
exec(code, module.__dict__)
class SimpleFinder:
def find_spec(self, fullname, path, target=None):
if fullname == "mymodule":
spec = importlib.util.spec_from_loader(fullname, StringLoader())
return spec
return None
sys.meta_path.insert(0, SimpleFinder())
# in_memory_importer.py
import sys
import importlib.abc
import importlib.util
import types
# 1) 我们的“虚拟文件系统” —— 模块名 -> 源代码
VFS = {
"virtual.mod1": """
value = 123
def hello():
return "hello from virtual.mod1"
""",
"virtual.subpkg.mod2": """
from ..mod1 import value
def get():
return f"mod2 sees {value}"
"""
}
# 2) 实现 Finder(MetaPathFinder)
class VFSFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
# 如果我们的 VFS 中有该模块名,则返回一个 ModuleSpec
if fullname in VFS:
loader = VFSLoader(fullname)
# submodule_search_locations 用于把它当作包(如果需要)——这里不设置包搜索
return importlib.util.spec_from_loader(fullname, loader, origin="vfs")
# 支持把 virtual 包作为包(使 virtual.subpkg 可作为包)
if fullname in ("virtual", "virtual.subpkg"):
# 标记为 package(设置 submodule_search_locations 为 list)
loader = VFSLoader(fullname)
return importlib.util.spec_from_loader(fullname, loader, origin="vfs", is_package=True)
return None
# 3) 实现 Loader(继承抽象基类)
class VFSLoader(importlib.abc.Loader):
def __init__(self, name):
self.name = name
def create_module(self, spec):
# 返回 None 表示使用默认 module 创建器(types.ModuleType)
return None
def exec_module(self, module):
# 在 exec_module 中执行模块代码,填充 module.__dict__
src = VFS.get(self.name)
if src is None:
# 如果是 package 且没有源,允许留空,或者可以 set __path__ 等
if getattr(module, "__package__", None):
module.__path__ = [] # 代表这是个 package(示例)
return
raise ImportError(f"No source for {self.name}")
# 执行源代码(注意在真实场景需要考虑安全)
code = compile(src, f"<vfs:{self.name}>", "exec")
exec(code, module.__dict__)
# 4) 安装到 sys.meta_path(前置优先)
sys.meta_path.insert(0, VFSFinder())
# 5) 使用示例
if __name__ == "__main__":
import virtual.mod1
print("virtual.mod1.value:", virtual.mod1.value)
print("virtual.mod1.hello():", virtual.mod1.hello())
# 导入子包模块示例(需要支持 package)
import virtual.subpkg.mod2
print("virtual.subpkg.mod2.get():", virtual.subpkg.mod2.get())
一个能加载 md 文件_markdown 的 import hook (来自AI)
能加载 markdown 文件的 import hook”
# md_import_hook.py
import sys
import os
import importlib.abc
import importlib.util
import types
# 可选:尝试导入 markdown 库以转换为 HTML(非必须)
try:
import markdown
except Exception:
markdown = None
class MarkdownLoader(importlib.abc.Loader):
"""
Loader:负责把 .md 文件的内容载入模块命名空间。
模块将包含:
- __file__ :源文件路径
- __text__ :原始 Markdown 文本
- __html__ :如果能转换则为 HTML 字符串,否则 None
"""
def __init__(self, fullname, path, is_package=False):
self.fullname = fullname
self.path = path
self.is_package = is_package
def create_module(self, spec):
# 使用默认的模块创建(types.ModuleType)。返回 None 表示使用默认创建器。
return None
def exec_module(self, module):
# 执行模块:读取 .md 内容并注入模块命名空间
module.__file__ = self.path
module.__package__ = self.fullname if self.is_package else self.fullname.rpartition('.')[0]
if self.is_package:
# 简单设置 __path__ (允许子模块继续通过 finder 找到基于此包的子模块)
module.__path__ = [os.path.dirname(self.path)]
with open(self.path, 'r', encoding='utf-8') as f:
text = f.read()
module.__text__ = text
if markdown:
# 若安装了 markdown 包则转换为 html
module.__html__ = markdown.markdown(text)
else:
module.__html__ = None
class MarkdownMetaFinder(importlib.abc.MetaPathFinder):
"""
MetaPathFinder:在 sys.path 每个 entry 下查找 <path_parts>.md 或 package dir with __init__.md
支持:
- import a.b.c -> search for a/b/c.md
- packages: if a/b is a dir and contains __init.md (or __init__.md), treat as package
"""
def __init__(self, suffix='.md'):
self.suffix = suffix
def _find_in_path(self, fullname, path_entry):
"""
在给定的 sys.path 条目下查找 fullname 对应的 .md 文件或 package 的 __init__.md
返回 (found_path, is_package) 或 (None, False)
"""
parts = fullname.split('.')
# 1) 先尝试把 fullname 作为普通模块文件: e.g. <path_entry>/a/b/c.md
candidate = os.path.join(path_entry, *parts) + self.suffix
if os.path.isfile(candidate):
return candidate, False
# 2) 再尝试作为 package:目录存在且目录下有 __init.md 或 __init__.md
pkg_dir = os.path.join(path_entry, *parts)
if os.path.isdir(pkg_dir):
for init_name in ('__init' + self.suffix, '__init__.md'):
init_path = os.path.join(pkg_dir, init_name)
if os.path.isfile(init_path):
return init_path, True
return None, False
def find_spec(self, fullname, path, target=None):
# 忽略内置或顶层名字过短的情况(可根据需要放宽)
# 在每个 sys.path 条目查找
for entry in sys.path:
# 忽略非文件系统 entry(比如 zipimport 可能是 zip 路径),但我们仍尝试路径字符串
if not isinstance(entry, str):
continue
# fast skip
if not os.path.exists(entry):
continue
found, is_pkg = self._find_in_path(fullname, entry)
if found:
loader = MarkdownLoader(fullname, found, is_package=is_pkg)
# 如果是 package,需要设置 submodule_search_locations
if is_pkg:
spec = importlib.util.spec_from_loader(fullname, loader, origin=found, is_package=True)
# 指定 submodule_search_locations (这样 import a.b.c 会在 a 的 __path__ 下继续查找)
spec.submodule_search_locations = [os.path.dirname(found)]
else:
spec = importlib.util.spec_from_loader(fullname, loader, origin=found)
return spec
# 没找到则返回 None,让下一个 finder 处理
return None
# 安装到 sys.meta_path(放到最前面以优先处理 .md)
_finder = MarkdownMetaFinder()
if _finder not in sys.meta_path:
sys.meta_path.insert(0, _finder)
# ------------------------------
# 下面是一个自包含的 demo:创建临时 md 文件并演示 import
# 只在作为脚本执行时运行(import 时不运行)
if __name__ == '__main__':
import tempfile, shutil
demo_dir = os.path.join(os.path.dirname(__file__), 'md_demo')
os.makedirs(demo_dir, exist_ok=True)
# 创建一个普通模块 md: md_demo/hello.md -> import hello
with open(os.path.join(demo_dir, 'hello.md'), 'w', encoding='utf-8') as f:
f.write("# Hello\nThis is a markdown module.\n\n- item1\n- item2\n")
# 创建一个 package: md_demo/pkg/__init.md 以及 pkg/sub.md
pkg_dir = os.path.join(demo_dir, 'pkg')
os.makedirs(pkg_dir, exist_ok=True)
with open(os.path.join(pkg_dir, '__init.md'), 'w', encoding='utf-8') as f:
f.write("# pkg init\nvalue = 42\n")
with open(os.path.join(pkg_dir, 'sub.md'), 'w', encoding='utf-8') as f:
f.write("This is sub module inside pkg.\n")
# 把 demo_dir 放入 sys.path(优先)
if demo_dir not in sys.path:
sys.path.insert(0, demo_dir)
# 演示导入
print("sys.path[0] = ", sys.path[0])
import hello
print("hello.__file__ =", hello.__file__)
print("hello.__text__[:60] =", hello.__text__[:60].replace('\n','\\n'))
print("hello.__html__ is None? ", hello.__html__ is None)
import pkg
print("pkg.__file__ =", pkg.__file__)
print("pkg.__path__ =", getattr(pkg, '__path__', None))
print("pkg.__text__[:40] =", pkg.__text__[:40].replace('\n','\\n'))
import pkg.sub
print("pkg.sub.__file__ =", pkg.sub.__file__)
print("pkg.sub.__text__[:40] =", pkg.sub.__text__[:40].replace('\n','\\n'))
# cleanup optional (取消注释若想删除 demo 文件)
# shutil.rmtree(demo_dir)