根本原因是 python 的 sys.path 未包含预期父目录;应统一用绝对导入、以项目根目录为基准,配合 __init__.py 和 python -m 启动,避免手动修改 sys.path。

模块拆分时 import 路径总报 ModuleNotFoundError
根本原因是 Python 的 sys.path 没包含你认为“理所当然”的父目录,不是路径写错了,而是解释器根本没去那里找。项目一深,from src.utils import helper 这种写法在不同执行位置(比如从根目录 run、从 tests 目录 run、用 pytest 执行)行为不一致。
实操建议:
- 统一用绝对导入:所有
import都基于项目根目录,根目录下必须有__init__.py(哪怕空文件),并确保运行时当前工作目录是根目录(或通过-m方式启动) - 避免在代码里手动改
sys.path.append(...)—— 这会让模块位置变得隐式且难调试 - 用
python -m src.main启动,而不是python src/main.py;前者把src当作包,后者把它当脚本,加载机制完全不同 - 如果必须支持多入口,用
setuptools配pyproject.toml中的[project.entry-points."console_scripts"],让 pip install -e . 后能直接调命令
什么时候该建子包而不是平铺一堆 .py 文件
不是看文件数量,而是看职责是否形成可复用、可隔离的语义单元。比如 auth 里混着数据库模型、HTTP 路由、密码策略,后期想抽出来给另一个项目用?不可能。但如果你把 auth/models.py、auth/providers.py、auth/exceptions.py 放一起,并对外只暴露 auth.get_user() 和 auth.InvalidTokenError,那它就具备了子包价值。
判断信号:
立即学习“Python免费学习笔记(深入)”;
CPWEB企业网站管理系统(以下称CPWEB)是一个基于PHP+Mysql架构的企业网站管理系统。CPWEB 采用模块化方式开发,功能强大灵活易于扩展,并且完全开放源代码,面向大中型站点提供重量级企业网站建设解决方案。CPWEB企业网站管理系统 2.2 Beta 测试版本,仅供测试,不建议使用在正式项目中,否则发生任何的后果自负。
- 多个模块频繁互相
from . import xxx,且外部几乎不直接引用内部模块 → 适合收进子包 -
__init__.py开始承担“门面”职责:聚合导出接口、设置默认配置、触发初始化逻辑 - 测试目录结构开始镜像源码结构(
tests/auth/test_providers.py对应auth/providers.py)→ 结构已自然分层 - CI 中想单独运行某块测试(如
pytest tests/auth),但当前平铺导致无法精准切分
setup.py 已淘汰,pyproject.toml 怎么配才算“生产可用”
很多团队配完 pyproject.toml 就以为万事大吉,结果别人 clone 下来 pip install -e . 报错,或者 CI 里依赖版本冲突。问题常出在三个字段没对齐:包发现方式、依赖声明粒度、Python 版本约束。
关键配置项和避坑点:
-
[build-system]必须用build-backend = "setuptools.build_meta"(别用 hatch 或 pdm 的 backend,除非全团队统一工具链) -
[project]下的requires-python = ">=3.9"要和 CI 环境、部署环境严格一致;dependencies别写requests>=2.25,而要写requests>=2.25,,防止意外升级破坏兼容性 -
[project.optional-dependencies]是解耦的好地方:比如dev = ["pytest", "black"]、aws = ["boto3"],用户按需装,不会让基础包变重 - 别漏掉
[project.urls],尤其是"Repository" = "https://..."—— 这是 pip show 时唯一能追溯来源的地方
大型项目里 __init__.py 不再只是“让目录变包”的占位符
它实际是模块 API 的第一道门禁。很多人留空或只写几行 from .xxx import yyy,结果用户 import mypkg 后得自己翻子模块,而 from mypkg import get_client 这种简洁用法根本不存在。
应该做的事:
- 在顶层
__init__.py中显式 re-export 核心类/函数,控制公共接口范围;内部模块名(如_utils、internal)加下划线前缀,不在__all__里列,就是明确告诉使用者:“别依赖这个” - 利用
__getattr__延迟加载(Python 3.7+):对重型子模块(如含 C 扩展的fastmath),首次访问才导入,避免import mypkg就触发全部初始化 - 在
__init__.py末尾做轻量级检查:比如确认os.getenv("ENV")存在,或 warn 用户当前 Python 版本接近 EOL —— 错误提示比散落在各处更早、更集中
越大的项目,__init__.py 越不能“省事”。它不是语法要求,是 API 设计的一部分。









