HOWTO · Python

Python 导入一个目录中的所有模块

本教程讨论如何在 Python 中导入目录中的所有模块。

本页内容

要从一个目录导入模块,可以将该目录视为 Python 包。模块集合固定时,可在包的 __init__.py 中列出导入语句;如果事先不知道模块名称,则可使用 importlib 按包限定名称发现并导入模块。

在 Python 中导入一个目录中的所有模块

包可以组织相关模块,并为它们提供方便的统一接口。例如,包 a 可以包含 x.py、y.py 和 z.py,以及 __init__.py。Python 导入包时会运行初始化文件,但不会自动导入同级文件:如果希望这些模块作为包的属性使用,就要在其中显式导入它们。

下面的独立示例会创建该包,在初始化文件中导入其中的模块,并通过 a 访问每个模块:

from pathlib import Path
import sys
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temp_dir:
    package_dir = Path(temp_dir) / "a"
    package_dir.mkdir()
    (package_dir / "x.py").write_text("value = 'X'\n", encoding="utf-8")
    (package_dir / "y.py").write_text("value = 'Y'\n", encoding="utf-8")
    (package_dir / "z.py").write_text("value = 'Z'\n", encoding="utf-8")
    (package_dir / "__init__.py").write_text(
        "from . import x, y, z\n", encoding="utf-8"
    )
    sys.path.insert(0, temp_dir)
    try:
        import a
        assert (a.x.value, a.y.value, a.z.value) == ("X", "Y", "Z")
    finally:
        sys.path.remove(temp_dir)
        for module_name in list(sys.modules):
            if module_name == "a" or module_name.startswith("a."):
                sys.modules.pop(module_name, None)

在提供的、与当前候选版本绑定的执行记录中,两个示例均成功完成,退出代码为 0,且没有标准输出。

在 __init__.py 中使用相对导入后,执行 import a 即可通过 a.x、a.y 和 a.z 使用这些模块。显式列出导入项便于审查,也让包的接口更可预测。初始化文件应保持精简:导入包时会运行其初始化文件以及所导入模块的顶层代码,因此不要在其中放入无关操作或开销较大的副作用。

使用 importlib 进行高级模块导入

如果程序的其他部分会随时间添加模块,固定的导入列表可能就不合适。面向包的发现方式可以找到包中的直接模块,并按限定名称加载它们。下面这个独立示例会创建一个包,使用 pkgutil 发现其中的模块,并将每个已导入的模块存入字典:

from pathlib import Path
import importlib
import pkgutil
import sys
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temp_dir:
    package_dir = Path(temp_dir) / "plugins"
    package_dir.mkdir()
    (package_dir / "__init__.py").write_text("", encoding="utf-8")
    (package_dir / "alpha.py").write_text("value = 1\n", encoding="utf-8")
    (package_dir / "beta.py").write_text("value = 2\n", encoding="utf-8")
    sys.path.insert(0, temp_dir)
    try:
        package = importlib.import_module("plugins")
        modules = {
            item.name: importlib.import_module(f"{package.__name__}.{item.name}")
            for item in pkgutil.iter_modules(package.__path__)
            if not item.ispkg
        }
        assert sorted(modules) == ["alpha", "beta"]
        assert modules["alpha"].value == 1
        assert modules["beta"].value == 2
    finally:
        sys.path.remove(temp_dir)
        for module_name in list(sys.modules):
            if module_name == "plugins" or module_name.startswith("plugins."):
                sys.modules.pop(module_name, None)

此示例不会写入标准输出:

使用 modules["alpha"] 可访问已导入的模块对象,然后读取它的属性或调用它的函数。上面的发现逻辑只涵盖包中的直接模块,不会处理子包;如果也要加载嵌套包,请有意调整这一策略。它不会按应用特定的命名约定筛选模块;如果只想加载选定的插件,请添加判断条件。每个发现到的模块都会在导入时执行代码,可能需要依赖项或运行自己的初始化逻辑。依赖项缺失或模块代码出错,都可能使加载循环中断。模块集合确实需要动态扩展时,动态加载很有用;如果模块集合已知,显式导入则更清晰,也更易维护。由于导入会在当前进程中执行,只应从可信的包内容中发现模块。对于初始化开销较大的包,应将插件注册与模块加载分开,并说明调用方可以依赖哪些名称。

总结

如果包的公开模块已知,请在 __init__.py 中显式导入。如果程序必须加载不断变化的模块集合,请使用面向包的 importlib 发现方式,并向调用方说明发现规则和导入时的副作用。