imp 是历史导入接口:Python 3.4 起弃用,Python 3.12 已移除,最后包含它的版本系列是 Python 3.11。维护旧代码时应根据实际意图迁移到 importlib、types 或 sys,不能只把模块名前缀换掉。Python 3.12 移除说明
原稿中的 exec(spec.loader.exec_module(module)) 会先执行模块,再尝试执行通常为 None 的返回值,随后报错。正确流程只调用一次 exec_module();它执行模块代码,结果主要体现在模块命名空间中,不是返回一份等待 exec() 的源代码。
按用途迁移旧接口
跳转到“按用途迁移旧接口”历史 imp 接口 | 迁移方向 | 不能忽略的差别 |
|---|---|---|
new_module(name) | types.ModuleType(name);若有规格则用 module_from_spec() | 创建对象不等于执行源码或注册进 sys.modules |
get_magic() | importlib.util.MAGIC_NUMBER | 是当前字节码格式的标识,不是文件内容校验或可信证明 |
get_tag() | sys.implementation.cache_tag | 是解释器缓存标签,可能为 None,不同于 .pyc 的魔数 |
cache_from_source() | importlib.util.cache_from_source() | 返回缓存路径,不要求源码存在,也不创建 .pyc |
source_from_cache() | importlib.util.source_from_cache() | 需要符合缓存路径约定,不是对任意 .pyc 名称去掉一个字母 |
get_suffixes() | importlib.machinery 的源码、字节码、扩展后缀列表 | 新接口不是原来的 (后缀, 模式, 类型) 三元组列表 |
NullImporter | 审查原来的路径钩子/拒绝导入需求 | 没有适合所有调用点的一行替换,不需要时移除旧适配层 |
load_source() | 建立文件规格、创建模块、登记并执行 | 名称冲突、失败清理和包上下文需要处理 |
load_compiled() | 通常恢复源码并按普通导入;必要时审查字节码加载器 | .pyc 与解释器版本/实现相关,加载它同样会执行代码 |
load_package() | 优先 importlib.import_module() 与正常包路径 | 按文件加载单模块不自动建立完整父包关系 |
load_module() | 以 ModuleSpec 和 exec_module() 为核心的新协议 | 不要迁移到加载器上同样陈旧的 load_module() 方法 |
find_module() | importlib.util.find_spec() | 返回规格而非旧三元组;查询子模块时可能导入父包 |
reload(module) | importlib.reload(module) | 旧的外部引用和实例不会自动替换 |
init_builtin(name) | 通常使用 importlib.import_module(name) | 不需要把内置模块当普通源文件加载 |
正常按模块名称加载最简单:importlib.import_module("json") 返回该模块;加载子模块时返回指定子模块,而不是总返回顶层包。应先确认包安装在所选解释器环境中,参见 sys 与搜索路径。
只加载已选定的本地源文件
跳转到“只加载已选定的本地源文件”下面给出一个范围明确的函数:单线程中,将可信 .py 文件加载成一个新的顶层模块名称;拒绝已有名称,不支持带点的包名,也不提供包内相对导入适配。它不会扫描或自动执行目录中其他文件。
import importlib.utilfrom pathlib import Pathimport sysfrom tempfile import TemporaryDirectory
def load_source_file(name: str, filename): if not isinstance(name, str) or not name.isidentifier(): raise ValueError("需要不含点的单个模块名称") if name in sys.modules: raise ValueError("模块名称已经存在") filename = Path(filename).resolve(strict=True) if filename.suffix != ".py" or not filename.is_file(): raise ValueError("需要一个已经存在的 .py 文件") spec = importlib.util.spec_from_file_location(name, filename) if spec is None or spec.loader is None: raise ImportError("无法建立源文件加载规格") module = importlib.util.module_from_spec(spec) sys.modules[name] = module try: spec.loader.exec_module(module) except BaseException: if sys.modules.get(name) is module: del sys.modules[name] raise return module
name = "_vitalogos_import_demo"with TemporaryDirectory(prefix="vitalogos-import-") as directory: filename = Path(directory) / "example.py" filename.write_text( "from __future__ import annotations\n" "from dataclasses import dataclass\n" "@dataclass\n" "class Point:\n" " x: int\n" " y: int\n" "def total(point):\n" " return point.x + point.y\n", encoding="utf-8", ) module = load_source_file(name, filename) try: point = module.Point(2, 3) assert module.total(point) == 5 assert sys.modules[name] is module assert module.__spec__.origin == str(filename.resolve()) finally: if sys.modules.get(name) is module: del sys.modules[name]print("source loaded once under its module name")在执行前放入 sys.modules 有实际意义:执行期间可能出现按自身名称导入、装饰器检查模块或其他依赖模块身份的行为。上例含有 dataclass 与字符串化注解,因此也会覆盖这类需求。直接导入源文件的官方流程
失败时这里只清理由本函数登记的同一个模块对象,不回滚已写文件、已发请求或它导入的其他模块。加载的源文件必须来自调用方信任的位置;importlib 不提供隔离执行。示例也没有设计并发名称分配、模块自行替换 sys.modules 条目或生产插件生命周期。
加载单文件与安装包是两件事。带 from .helper import ... 的源码依赖父包上下文,应优先修复包结构并按名称导入;给它随意取个顶层名字并不能解决相对导入问题。包含原生扩展的包还可能有 ABI 与加载器约束。
创建模块对象与计算缓存路径
跳转到“创建模块对象与计算缓存路径”这段代码只创建内存对象并计算缓存路径,不写 .pyc,也不执行外部源码:
import importlib.machineryimport importlib.utilfrom pathlib import Pathimport sysimport types
name = "_vitalogos_empty_demo"assert name not in sys.modulesmodule = types.ModuleType(name, "an in-memory example")module.answer = 42assert module.answer == 42assert name not in sys.modules
source = str(Path("example.py"))cache = importlib.util.cache_from_source(source)assert importlib.util.source_from_cache(cache) == sourceassert isinstance(importlib.util.MAGIC_NUMBER, bytes)assert ".py" in importlib.machinery.SOURCE_SUFFIXESassert ".pyc" in importlib.machinery.BYTECODE_SUFFIXESprint("cache tag:", sys.implementation.cache_tag)print("cache path:", cache)这份验证使用具有缓存标签的 CPython 3.11;没有缓存标签的实现可能不能使用相同的缓存路径操作。缓存文件路径通常包含 __pycache__ 与实现标签,不能把 source_from_cache("example.pyc") 当作适用于所有布局的逆变换。
.pyc 并不是可以跨 Python 版本随意搬运的通用二进制格式。一个文件具有正确魔数也不说明代码可信;从字节码加载仍然执行代码。源码可用时,保留源码并让当前解释器按自己的缓存规则处理,通常更容易维护。
reload 的限制
跳转到“reload 的限制”importlib.reload() 重新执行模块代码,但不是应用级热更新。模块字典会被保留,因此新源码里删除的旧名称可能仍留在字典中;通过 from module import function 取得的旧引用、原类的旧实例也不会自动切换。重新导入已经在 sys.modules 中的名称,通常只是复用缓存,并不等同于 reload。
下面用一个自建临时模块展示这些差别;修改后令源码长度发生变化,并使查找器缓存失效,避免把测试是否命中旧字节码与 reload 的对象语义混在一起。所有搜索路径和模块登记在结束时恢复。
import importlibfrom pathlib import Pathimport sysfrom tempfile import TemporaryDirectory
name = "_vitalogos_reload_demo"assert name not in sys.modulesoriginal_path = list(sys.path)with TemporaryDirectory(prefix="vitalogos-reload-") as directory: filename = Path(directory) / (name + ".py") filename.write_text( "old_only = 1\n" "class Value:\n" " def read(self): return 10\n", encoding="utf-8", ) try: sys.path.insert(0, directory) importlib.invalidate_caches() module = importlib.import_module(name) old_class = module.Value old_instance = old_class() filename.write_text( "# changed source length deliberately\n" "class Value:\n" " def read(self): return 200\n", encoding="utf-8", ) importlib.invalidate_caches() reloaded = importlib.reload(module) assert reloaded is module assert module.Value is not old_class assert old_instance.read() == 10 assert module.Value().read() == 200 assert module.old_only == 1 finally: sys.path[:] = original_path sys.modules.pop(name, None) importlib.invalidate_caches()assert sys.path == original_pathprint("reload replaced definitions, not old references")这是单线程的自建模块演示,不是业务代码的热更新方案。invalidate_caches() 使查找器重新发现文件,不负责删除所有字节码缓存,也不会清空 sys.modules。任意按文件加载的自定义模块也未必能由普通查找器再次找到,因而不能保证可直接 reload()。内置模块、原生扩展、长期运行服务和并发插件系统需要单独设计生命周期。importlib.reload() 语义