HOWTO · Python
从 Python 中的子目录导入模块
本教程演示了如何在 Python 中从子目录导入一个文件
本页内容
要从项目子目录导入模块,先让其父包可被导入,再使用模块的点分名称。对于一次性脚本,也可以将模块所在目录添加到 sys.path。相对导入适用于作为包的一部分加载的模块。
Python 如何查找模块
Python 处理 import 时,会在运行中的解释器进程所持有的列表 sys.path 所列的位置中查找请求的模块。列表的初始条目取决于 Python 的启动方式;当前工作目录和脚本所在位置都可能影响这些条目。项目子目录即使与另一个项目文件位于同一目录中,也不会因此自动成为可导入目录。
PYTHONPATH 是一个环境变量,Python 启动时可以据此加入搜索位置。它与 sys.path 不同:在代码中修改 sys.path 会影响当前进程,而设置 PYTHONPATH 则会为使用该环境启动的进程配置搜索位置。对于大多数应用代码,使用包导入能让项目结构更清晰,也不必依赖特定机器上的路径设置。
从包的子目录导入模块
假设项目中有 project/reports/summary.py,而 summary.py 定义了一个名为 label 的函数。只要 Python 能访问项目根目录,就可以通过 project.reports.summary 导入该模块。__init__.py 文件可以包含初始化代码;在这个示例中,它们用于将这些目录标记为常规包。这里展示的是一种明确的包结构,并不表示所有 Python 包都必须使用这些文件。
运行以下示例。示例代码中的 Monthly summary 标签及其输出保持英文:
本节中的代码以及显示的标签或输出仍为英文。
from pathlib import Path
from tempfile import TemporaryDirectory
import sys
with TemporaryDirectory() as folder:
root = Path(folder)
reports = root / "project" / "reports"
reports.mkdir(parents=True)
(root / "project" / "__init__.py").write_text("")
(reports / "__init__.py").write_text("")
(reports / "summary.py").write_text("def label():\n return 'Monthly summary'\n")
sys.path.insert(0, str(root))
from project.reports.summary import label
print(label())
该示例会在临时项目根目录中创建包文件,将该根目录加入当前进程的搜索位置,然后通过包名导入函数。在实际项目中,请将包目录放在项目根目录下,并在项目根目录可被导入的环境中运行 Python。如果导入时提示找不到模块,请检查点分隔的模块名称、文件和目录名称,以及项目根目录是否位于该进程使用的搜索位置中。
将目录添加到 sys.path
如果某个独立模块没有组织成包,可以在导入前将模块所在目录添加到搜索路径中。sys.path.insert(0, ...) 会把该位置放到当前进程搜索顺序的靠前处,因此其优先级高于后续条目。对于特定脚本或受控的运行环境,可以把这种做法作为有针对性的权宜方案;它不会安装模块,也不会配置之后启动的 Python 进程。
运行以下示例。示例代码中的 Project report 标签及其输出保持英文:
本节中的代码以及显示的标签或输出仍为英文。
from pathlib import Path
from tempfile import TemporaryDirectory
import sys
with TemporaryDirectory() as folder:
module_dir = Path(folder) / "helpers"
module_dir.mkdir()
(module_dir / "formatting.py").write_text("def title():\n return 'Project report'\n")
sys.path.insert(0, str(module_dir))
import formatting
print(formatting.title())
这种做法会把一个范围较大的目录加入导入搜索,因此 Python 可能意外选中另一个同名文件。对于可复用的项目代码,最好使用名称独特的包。如果启动环境需要为多个进程提供某个目录,也可以使用 PYTHONPATH;但它并不是进程内 sys.path 列表的另一个名称。
在包内使用相对导入
导入语句中的前导点会根据当前包解析。例如,当 app 作为 project.app 加载时,project/app.py 可以使用 from .reports.summary import label 导入 project/reports/summary.py。如果直接通过 python project/app.py 启动顶层脚本,它没有可供该前导点导入使用的包上下文,因此导入会失败。可以从包含 project 的目录运行 python -m project.app,也可以改用绝对包导入。
运行以下示例。示例代码中的 Monthly summary 标签及其输出保持英文:
本节中的代码以及显示的标签或输出仍为英文。
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import sys
with TemporaryDirectory() as folder:
root = Path(folder)
reports = root / "project" / "reports"
reports.mkdir(parents=True)
(root / "project" / "__init__.py").write_text("")
(reports / "__init__.py").write_text("")
(reports / "summary.py").write_text("def label():\n return 'Monthly summary'\n")
(root / "project" / "app.py").write_text(
"from .reports.summary import label\nprint(label())\n"
)
result = subprocess.run(
[sys.executable, "-m", "project.app"],
cwd=root,
check=True,
capture_output=True,
text=True,
)
print(result.stdout, end="")
这里 Python 将应用作为包模块启动,因此相对导入有明确的包父级。对于紧密相关、可能作为整体移动的模块,相对导入很实用;在包外阅读代码时,绝对导入通常更容易辨认。
选择合适的方法
普通项目代码通常适合使用包导入:它能清楚展示模块层级,也不必依赖临时添加的目录。在包内,如果需要根据共享的包来引用相邻模块,可以选择相对导入;如果完整包名能让依赖关系更清楚,则可以选择绝对导入。对于范围明确的脚本或运行时需求,可以将目录添加到 sys.path,同时留意该目录在搜索顺序中的位置,因为它可能影响 Python 找到哪个同名模块。
若要从多个子目录导入模块,可以为每个区域设置包路径,再用包限定名称导入各个模块,例如 project.reports.summary 和 project.data.reader。请确认该进程能够访问项目根目录。避免将多个通用目录添加到 sys.path,因为名称可能冲突,导入来源也会更难追踪。这些示例会在本地创建所需文件,让每种导入配置一目了然;你可以根据自己的项目调整包名和启动环境。