动态加载通常有两种场景:模块已经位于可导入的 Python 包中,或者只有一个任意路径下的 .py 文件。前者使用 import_module(),后者使用 spec_from_file_location()。
按包名加载
目录结构如下。modules/__init__.py 不能省略,它让 modules 成为普通包。
.
├── main.py
└── modules
├── __init__.py
├── module_1.py
└── module_2.py
两个模块都提供统一入口:
# modules/module_1.py
def echo():
print("This is module 1")
main.py 发现包内模块并逐个调用:
from importlib import import_module
from pathlib import Path
PACKAGE = "modules"
PACKAGE_DIR = Path(__file__).parent / PACKAGE
for module_file in sorted(PACKAGE_DIR.glob("*.py")):
if module_file.name == "__init__.py":
continue
module = import_module(f"{PACKAGE}.{module_file.stem}")
module.echo()
这里应使用 Path.stem 获取不带扩展名的文件名。filename.rstrip(".py") 会按字符集合删除末尾的 p、y 和 .,并不等同于删除扩展名。
按文件路径加载
插件文件不在当前包或 sys.path 中时,可以直接从路径创建模块规范:
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
def load_module(file_path: Path):
module_name = f"plugin_{file_path.stem}"
spec = spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None:
raise ImportError(f"cannot load module from {file_path}")
module = module_from_spec(spec)
spec.loader.exec_module(module)
return module
for file_path in sorted(Path("/opt/my-app/plugins").glob("*.py")):
load_module(file_path).echo()
加载模块会执行模块顶层代码,因此路径和文件内容必须来自可信来源。不要用字符串拼接配合 exec() 实现导入;它既绕过正常导入语义,也会把不可信模块名变成代码执行入口。
参考
内置函数 — Python 3 文档
5. 导入系统 — Python 3 文档
importlib — import 的实现
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。