05 模块、包与依赖管理
大约 3 分钟编程语言Python模块包依赖管理
05 模块、包与依赖管理
模块化的目标是控制知识流动:调用者只依赖公开接口,实现细节可以独立变化。
1. 模块、包和脚本入口
一个 .py 文件就是模块;包含多个模块的目录通常就是包。
# src/ops_inspector/cli.py
from ops_inspector.checks import check_disk
def main() -> int:
result = check_disk("/")
print(result)
return 0
if __name__ == "__main__":
raise SystemExit(main())if __name__ == "__main__" 让文件既能被导入,也能作为脚本运行。不要在被导入时自动连接数据库、发请求或启动服务。
2. 推荐项目结构
ops-inspector/
├── pyproject.toml
├── README.md
├── src/
│ └── ops_inspector/
│ ├── __init__.py
│ ├── cli.py
│ ├── checks.py
│ └── models.py
└── tests/
├── test_checks.py
└── test_cli.py使用 src/ 布局可以让测试更接近已安装包的真实导入行为,减少“当前目录刚好能导入”的假象。
3. 导入规则
优先使用绝对导入:
from ops_inspector.checks import check_disk相对导入可以表达同一包内部的层次,但跨顶层包使用绝对导入更清晰:
from .models import CheckResult导入系统会查询 sys.path。临时向 sys.path 注入目录容易产生同名模块覆盖问题,应优先通过安装项目或明确调整启动方式解决。
4. 公共 API
包根目录可以明确导出稳定接口:
# src/ops_inspector/__init__.py
from ops_inspector.checks import check_disk
from ops_inspector.models import CheckResult
__all__ = ["CheckResult", "check_disk"]_private.py、单下划线名称和 __all__ 都是约定,不是安全边界。真正的边界来自稳定接口、测试和版本策略。
5. 资源文件
包内资源不要依赖源码目录的当前工作路径:
from importlib.resources import files
def read_default_config() -> str:
resource = files("ops_inspector").joinpath("defaults.toml")
return resource.read_text(encoding="utf-8")应用程序面向用户的配置文件放在操作系统约定目录;随代码发布的模板、Schema 和静态资源才放进包内。
6. 依赖分层
常见依赖层:
- 运行时依赖:程序运行必不可少。
- 开发依赖:测试、静态检查、调试。
- 构建依赖:构建 wheel 或生成产物。
- 可选依赖:数据库驱动、云平台 SDK、导出格式等。
依赖越多,攻击面、升级成本和启动时间越大。一个模块能由标准库可靠完成,就不要为了少量便利引入大型依赖。
7. 依赖锁定
pyproject.toml 描述项目兼容范围,锁文件描述一次验证过的完整依赖图。应用部署应尽量使用同一个锁文件重建环境:
python -m pip install --require-hashes -r requirements.lock工具可以不同,但必须保证:
- 直接依赖和间接依赖都有确定版本或哈希。
- CI 和生产使用相同平台约束。
- 依赖扫描能定位到实际使用的包。
- 升级通过测试后提交锁文件变化。
8. 导入循环
循环导入常见表现是模块部分初始化。修复方式通常不是扩大延迟导入,而是重新划分层次:
- 把共享类型移到无业务依赖的模块。
- 让调用者注入对象,避免下层反向导入上层。
- 将 import 从模块顶层延迟到函数内部只作为最后手段。
9. 常见陷阱
- 模块导入时执行网络请求或文件写入。
- 包名、模块名与标准库或知名第三方包同名。
- 用
import *模糊依赖来源。 - 在同一进程中加载同一个包的两个不同版本。
- 直接把开发依赖和运行依赖混在一起。
