src/my_project双层结构非多此一举,它通过隔离源码与项目根、配合可编辑安装和pyproject.toml配置,确保导入稳定、避免命名冲突、支持可靠测试与发布。

为什么 src/my_project 这种双层结构不是“多此一举”
很多人看到 src/ 下还套一层同名包(比如 src/my_project/),第一反应是“绕路”。但问题不在路径长,而在 Python 的导入机制和发布逻辑。如果你把所有代码平铺在项目根目录下,import utils 在运行时可能意外导入了本地未安装的 utils.py,也可能被系统已装的第三方 utils 包干扰——尤其当测试或 CI 环境里执行 python -m pytest 时,当前目录自动加入 sys.path,导致模块解析顺序错乱。
用 src/my_project 结构,配合 pyproject.toml 中的 [project] requires-python = ">=3.9" 和 [build-system] 配置,能确保:
- 安装时只暴露 my_project 这个顶层包名(用户 pip install . 后只能 import my_project)
- 开发时通过 pip install -e . 建立可编辑安装,避免相对导入或 sys.path 手动追加
- 测试运行时,pytest 自动识别 src/ 为源码根,from my_project.utils import helper 始终稳定
包内分层要按“变化频率”而非“功能名词”切
别一上来就建 models/、views/、controllers/——这种 MVC 切法在纯 Python 服务里容易僵化。真正影响维护成本的是“谁会频繁改、谁改了会影响谁”。比如:
- 配置文件(
config/)和日志工具(utils/logger.py)几乎不随业务迭代变动,适合放到底层,被所有模块依赖 - 数据库模型(
domain/models.py)和 API 序列化器(api/serializers.py)经常一起改,但和定时任务逻辑(tasks/)基本无关,应划到同一变更域 - 第三方对接适配器(
adapters/payment_gateway.py)必须隔离,因为换支付渠道时只动它,不能波及核心订单服务
所以推荐先按“稳定性梯度”建包:core/(最稳)、domain/(业务实体)、services/(编排逻辑)、adapters/(最易变),而不是按技术角色硬套。
__init__.py 不只是“让文件夹变包”的空文件
在大型项目里,__init__.py 是显式声明公共接口的契约文件。比如 my_project/services/__init__.py 里写:
from .order_service import create_order, cancel_order from .user_service import get_user_profile <p><strong>all</strong> = ["create_order", "cancel_order", "get_user_profile"]</p>
这样外部 from my_project.services import * 才可控;否则 IDE 和静态检查工具(如 mypy)无法推断哪些符号是该模块对外承诺的。更关键的是,它防止内部重构时意外暴露私有实现——比如你把 _validate_payment 从 order_service.py 移到 utils/validation.py,只要不把它加进 __init__.py,上层代码就根本感知不到这个改动。
常见错误包括:__init__.py 留空导致子模块无法被正确发现;或过度导出(比如把整个 from . import * 放进去),造成循环导入风险。
tests/ 目录必须与 src/ 结构镜像,但不能复制导入路径
很多团队把 tests/ 放在根目录,里面建 test_services.py、test_adapters.py,看着整齐,但实际写测试时极易出错。正确做法是让 tests/ 的目录层级与 src/my_project/ 对齐,例如:
tests/
├── __init__.py
├── test_core.py
├── domain/
│ ├── __init__.py
│ └── test_models.py
├── services/
│ ├── __init__.py
│ └── test_order_service.py
└── adapters/
├── __init__.py
└── test_payment_gateway.py
这样每个测试文件都能用绝对导入(from my_project.services.order_service import create_order),和生产代码一致。如果测试里写 from ..services import order_service 这类相对导入,一旦 pytest 工作目录切换(比如从项目根跑 vs 从 tests 目录跑),就会报 ImportError: attempted relative import with no known parent package。
容易忽略的一点:CI 脚本里必须显式指定 --import-mode=importlib(pytest 7.0+ 默认值),否则某些插件可能仍走旧路径解析逻辑,导致测试通过但本地复现失败。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











