Python:项目从脚本到工程,虚拟环境、依赖管理、包结构怎么组织
Python 从脚本走向工程,最容易出现的错觉是:只要目录多几个文件夹,项目就算工程化了。
实际不是这样。真正决定一个 Python 项目能不能继续维护的,通常是下面这些基础问题:
- 解释器和依赖有没有跟项目绑定
- 入口是不是清楚
- 代码职责有没有开始分层
- 公共逻辑有没有抽出来
- 新增一个功能时,应该改哪里能不能一眼看出来
如果这些问题没处理,项目很快就会变成:
- 本地能跑,换机器就不行
- 代码分了文件,但职责还是混着
utils.py越来越大main.py什么都管- 测试、运行、导出全靠复制粘贴
这一篇直接围绕一个实际小项目展开:做一个巡检报告工具。
这个工具要完成这些事:
- 读取配置
- 调用若干巡检逻辑
- 汇总结果
- 导出报告
用这个场景把 Python 从脚本走向工程时最关键的四块串起来:
- 虚拟环境怎么和项目绑定
- 依赖管理怎么记录和复现
- 包结构怎么拆,职责才不会乱
- 入口文件怎么定,后面才不会到处都是
run_xxx.py
一、这篇文章要解决什么问题
读完这一篇,应该能独立完成这些动作:
- 给一个 Python 小项目建立独立虚拟环境
- 记录最小依赖并在新环境复现
- 把单文件脚本拆成包结构
- 明确“入口、业务、报告、配置”这些代码应该放哪
- 识别几类典型坏味道,例如所有逻辑堆在
main.py、公共函数全塞进utils.py
如果这些动作能独立做出来,Python 项目就不再只是“脚本集合”,而是开始有真正的工程骨架。
二、先看这个项目要长成什么样
先看目标结果,不然工程化很容易变成空谈。
希望最后项目结构类似这样:
1 | probe_report/ |
运行命令类似这样:
1 | python -m app.main |
输出类似这样:
1 | 巡检完成,总项数: 3 |
这个项目不大,但已经足够覆盖“从脚本到工程”的关键问题。
三、先看最容易失控的版本
很多项目第一版都像这样:
1 | import json |
第一版这样写没问题,但只要功能继续加,就会出现:
- 配置写死在代码里
- 巡检逻辑和导出逻辑缠在一起
- 主流程又做执行又做格式化
- 后面加测试时很难拆
这就是脚本该开始工程化的信号。
四、第一步不是拆目录,而是先把运行环境绑住
1. 先给项目建独立虚拟环境
1 | mkdir probe_report |
确认解释器位置:
1 | which python |
可能输出:
1 | /path/to/probe_report/.venv/bin/python |
这一步的意义很实际:
- 当前项目用哪套解释器明确了
- 依赖装在哪明确了
- 后面别人接手时复现方式也明确了
2. 一个实际错误:把依赖装到全局环境
错误做法:
1 | pip install pytest requests |
这会让依赖和项目脱钩。
实际做法:
1 | python -m pip install pytest requests |
这样至少能保证安装动作和当前解释器对应。
五、依赖管理怎么做才不乱
1. 先记录最小依赖
对于这个小项目,requirements.txt 可以很简单:
1 | pytest==8.2.0 |
2. 新环境复现动作要固定
1 | python -m pip install -r requirements.txt |
3. 一个实际错误:导出一堆和项目无关的包
有些项目的 requirements.txt 会长成这样:
1 | black==24.4.2 |
如果当前项目只是个巡检工具,这种依赖文件通常已经开始混入“本机装过但项目未必依赖”的包。
这个阶段最稳的做法不是追求复杂工具链,而是先把真正需要的依赖记清楚。
六、包结构为什么不能只靠感觉拆
1. 先看这几个职责
对这个小项目来说,至少有四块职责:
- 配置
- 巡检逻辑
- 结果模型
- 报告导出
如果这些职责都堆在 main.py,后面一定会开始复制粘贴。
2. 先拆成最小包结构
更合适的目录是:
1 | app/ |
分工这样定:
config.py:环境、token、输出目录probes.py:各类巡检函数models.py:统一结果结构report.py:展示和导出main.py:串主流程
3. 一个实际错误:目录拆了,职责没拆
例如:
config.py里也在跑请求report.py里也在判断业务是否成功main.py里还在拼 JSON
这就不是工程化,只是把乱代码切到了几个文件里。
七、先把统一结果结构抽出来
1. 不要每个地方都手写结果字典
app/models.py:
1 | def build_result(service, ok, message): |
2. 一个实际错误:结果字段不统一
错误写法:
1 | {"service": "auth", "ok": True, "message": "ok"} |
这会直接导致:
- 汇总代码要兼容多套字段
- 报告导出时容易出错
工程化的第一步之一,就是先把输出结构统一。
八、再把业务逻辑和导出逻辑拆开
1. 巡检逻辑放到 probes.py
app/probes.py:
1 | from app.models import build_result |
2. 报告逻辑放到 report.py
app/report.py:
1 | import json |
3. 主流程只负责串起来
app/main.py:
1 | from app.probes import probe_auth, probe_order, probe_config |
这时候主流程已经比单文件脚本清楚很多了。
九、配置应该放哪,才不会后面满地参数
1. 先把配置集中到一个地方
app/config.py:
1 | OUTPUT_FILE = "output/report.json" |
2. 一个实际错误:参数散在各处
错误写法通常像这样:
main.py里写一个timeout=3probes.py再写一个env="test"report.py又手写一次输出路径
这样后面一改环境,三个文件都要翻。
3. 实际做法:从配置统一读取
1 | from app.config import OUTPUT_FILE |
如果项目继续长大,再考虑配置文件或环境变量也不迟。
十、真正能运行的完整版本
下面给出一个最小但完整的工程化版本。
app/models.py:
1 | def build_result(service, ok, message): |
app/probes.py:
1 | from app.models import build_result |
app/report.py:
1 | import json |
app/config.py:
1 | OUTPUT_FILE = "output/report.json" |
app/main.py:
1 | from app.config import OUTPUT_FILE |
执行:
1 | python -m app.main |
输出:
1 | 巡检完成,总项数: 3 |
十一、怎么测试这一层是不是真的掌握了
这一层不能只看懂,要自己改一遍。
可以直接做这些动作:
- 增加一个新的巡检项
- 把输出路径改到
reports/目录 - 给
print_summary()和save_report()补最小测试 - 把配置从常量改成环境变量读取
- 给
probes.py增加一个失败原因不同的巡检项
如果这些改动都能独立完成,说明项目从脚本到工程这一步已经开始真正会了。
十二、一个实际排错场景
这类项目里一个非常常见的实际问题是:python app/main.py 能跑,python -m app.main 却又是另一种行为,或者反过来。
这时排查顺序应该很直接:
- 先看项目是不是已经按包结构组织
- 再看导入是不是写成了
from app.xxx import ... - 再看入口到底是按脚本运行还是按模块运行
如果项目已经开始走包结构,更稳定的执行方式通常是:
1 | python -m app.main |
因为这时 Python 会按模块方式理解整个包,而不是仅仅把某个文件当孤立脚本执行。
十三、一个实际练习
可以直接把这一篇变成一个完整练习。
练习目标:做一个“测试环境巡检报告项目”。
要求:
- 至少拆出
config.py、probes.py、report.py、main.py - 使用独立虚拟环境
- 记录
requirements.txt - 统一输出结构
- 生成一份报告文件
- 补一个实际错误场景,例如结果字段不统一或入口执行方式不一致
如果这个练习能独立做完,说明从脚本到工程这一步已经开始真正掌握。
十四、这篇文章学完以后,下一步应该补什么
如果这一篇已经能跟着做完,下一步最适合继续补的是:
- Python 做接口工具和数据处理脚本时,代码怎样写才不会越写越乱
- Python 写 HTTP 接口、命令行工具和定时任务时,目录和职责怎么拆
- pytest 的 fixture、mock 和测试数据怎么组织
因为到这一步,工程骨架已经有了。接下来更容易卡住的,是同一个项目开始出现多个入口、多种运行方式时,边界还能不能继续守住。
十五、结语
Python 项目从脚本到工程,最关键的不是先学多少工具,而是先把几件基础事情做稳:
- 解释器和依赖跟项目绑定
- 输出结构统一
- 入口清楚
- 职责分层
只要这几层先立住,哪怕项目继续长大,也还有继续整理的空间。