3. 项目目录结构设计

在 Python 项目打包与依赖管理工程中,目录结构不是“看起来整齐”的审美问题,而是直接决定导入路径、打包结果、测试发现和发布后用户能否正常使用的基础设施。一个项目在本地能跑,不代表安装后能跑;能安装,也不代表包名、模块名和入口点都符合发布规范。本节围绕三种布局取舍、命名规范和可导入/可发布验证展开。

一、flat 布局与 src 布局的本质差异

flat 布局把包目录直接放在项目根目录下,例如 mypkg/ 与 pyproject.toml、tests/ 平级;src 布局则把包放进 src/ 下,例如 src/mypkg/。两者最关键的差异不是目录层级,而是“当前工作目录是否恰好把包暴露在导入路径上”。

在 flat 布局中,从项目根目录运行 python -c "import mypkg" 时,当前目录会被加入 sys.path,于是导入成功。但这可能是一种假象:如果 mypkg 尚未被正确安装,或者打包配置漏掉了子包,本地仍然能导入。src 布局则把源码隔离在 src/ 下,根目录不再直接暴露包,只有安装后(或显式把 src 加入路径)才能导入。这种“先安装、再导入”的约束,能更早暴露打包配置错误。

取舍建议:库、框架、需要发布到 PyPI 或私有仓库的项目优先使用 src 布局;一次性脚本、教学示例、极小的内部工具可以使用 flat 布局。若项目已经采用 flat 布局且测试稳定,不必为了“规范”强行迁移,但必须补上安装后验证。

flat 布局 vs src 布局:导入路径与打包验证差异 flat 布局 project/ ├─ mypkg/ ← 包直接暴露 ├─ tests/ └─ pyproject.toml 根目录进入 sys.path 未安装也可能 import 成功 → 假阳性 src 布局 project/ ├─ src/ │ └─ mypkg/ ├─ tests/ └─ pyproject.toml 根目录不暴露包 → 必须安装后才能导入 可导入性与可发布性验证链 构建 wheel 隔离环境安装 外部目录导入 检查资源

二、包与模块的命名规范

命名规范分两层:文件系统层和 Python 标识符层。包目录名、模块文件名最终会变成 import 语句中的标识符,因此必须满足 Python 标识符规则:以字母或下划线开头,只含字母、数字、下划线,不能是关键字。实践中还应遵循 PEP 8 的建议:包名和模块名使用全小写,多个单词用下划线连接,例如 data_loader.py、http_client/。

需要特别注意几个容易踩坑的点。第一,避免与标准库或主流第三方库重名,例如 json.py、types.py、email.py、logging.py,一旦重名,导入顺序稍有变化就会覆盖标准库模块,产生难以定位的错误。第二,包名在发布到 PyPI 时会被规范化:大写转小写、下划线可被连字符替代,因此 My_Pkg 与 my-pkg 在索引上可能被视为同一项目,命名时应提前统一。第三,内部实现模块可用前导下划线标记私有,例如 _internal.py,但不要用双下划线开头结尾的名字作为普通模块名,那属于特殊方法命名空间。

对于命令行入口,pyproject.toml 中的 [project.scripts] 名称也应遵循同样的可读性要求,例如 mypkg-cli = "mypkg.cli:main",其中 mypkg.cli 是模块路径,main 是函数名,冒号两侧都不能写错。

三、可导入性与可发布性验证

验证的核心思想是:离开项目根目录,在一个干净环境中安装构建产物,再尝试导入。推荐流程如下。

# 1. 构建 wheel 与 sdist
python -m build

# 2. 创建隔离环境
python -m venv /tmp/verify_env
source /tmp/verify_env/bin/activate   # Windows: \Scripts\activate

# 3. 安装构建产物(不要用 -e,避免源码目录干扰)
pip install dist/mypkg-0.1.0-py3-none-any.whl

# 4. 切换到项目外目录,验证导入
cd /tmp
python -c "import mypkg; print(mypkg.__file__)"
python -c "from mypkg.cli import main; print(main)"

# 5. 验证包数据文件是否随包发布
python -c "import importlib.resources as r; print(r.files('mypkg').joinpath('data/config.json').is_file())"

判断依据很直接:第 4 步输出的路径应指向隔离环境的 site-packages,而不是原项目目录;如果仍指向项目源码,说明安装未生效或存在路径污染。第 5 步若返回 False,通常意味着 pyproject.toml 中缺少包数据声明,需要在构建后端配置中包含非 .py 文件。

在 CI 中可以把上述验证固化为一个独立 job:先 python -m build,再在全新容器里 pip install dist/*.whl,最后从 /tmp 执行导入断言。这样任何“本地能跑、安装后失败”的问题都会在合并前暴露。
不要用 pip install -e . 的结果作为可发布性证据。可编辑安装会把源码目录链接进环境,掩盖包数据缺失、子包未声明、命名冲突等问题。发布前必须至少验证一次非可编辑安装。

四、结构决策速查

判断项倾向 flat倾向 src
是否发布到包索引否,仅内部脚本是,需要 wheel/sdist
测试是否依赖根目录导入是,且不打算改否,愿意先安装再测
包数量与子包复杂度单包、少量模块多子包、含数据文件
对导入假阳性的容忍度较高低,要求尽早暴露

最后给出一份可直接对照的检查清单:包目录名是否为合法标识符且全小写;是否与标准库或已安装依赖重名;pyproject.toml 中 packages 或自动发现配置是否覆盖所有子包;包数据是否声明;是否在隔离环境完成一次非可编辑安装并从项目外成功导入;命令行入口是否能在安装后直接执行。把这六项纳入发布前检查,目录结构就不再是“风格选择”,而是可验证的工程约束。