在桌面软件(尤其是 Qt)中调用 Python 算法,是一个非常常见、但也非常容易"越做越复杂"的问题。我在实际设计中经历了一轮完整的取舍后,最终确定了一套低耦合、工程感强、不过度设计的方案,记录下来供参考。
一、为什么不用直接把 Python 嵌入到 C++/Qt?
最直观的做法,是在 Qt/C++ 里通过 CPython API 或 pybind11 直接调用 Python 代码。但只要 Python 代码稍微复杂一点,这条路的代价就会迅速放大:
- 运行时耦合极高:Python 版本、第三方库、虚拟环境、DLL 路径都会和 Qt 进程绑死。
- 稳定性差:numpy / torch / opencv 这类 C 扩展一旦崩,整个主程序一起崩。
- 线程模型复杂:Qt 多线程 + Python GIL,极容易出现卡死、随机问题。
- 发布困难:打包和升级成本非常高。
而且认真想一下,如果 Python 代码只是几行简单逻辑,那根本没必要用 Python;真正选择 Python 的原因,是它强大的算法生态。既然如此,就更应该把这部分复杂性隔离出去。
二、把 Python 当"算法服务",而不是"脚本"
最终我采用的思路是:
把 Python 算法做成一个独立的服务,Qt 通过 HTTP 调用它。
技术选型非常克制:
- Python 侧:FastAPI + Uvicorn
- Qt 侧:
QProcess+QNetworkAccessManager - 通信协议:HTTP + JSON(必要时二进制)
这个设计的核心思想是:
Qt 只依赖"接口协议",不依赖 Python 的任何运行细节。
这样做带来的好处非常明显:
- Python 崩了,Qt 不会崩
- 算法可以独立迭代、独立升级
- 未来甚至可以把算法放到远程或替换成 C++ 实现
- 调试体验极佳(FastAPI 自带
/docs)
三、为什么不用 RPC(gRPC 等)?
从"工程洁癖"角度,RPC 看起来很高级,但在这个场景下反而显得过重:
- 需要
.proto、代码生成、版本管理 - 调试成本远高于 HTTP
- 对 10 rps 这种本机调用,性能优势几乎没有意义
FastAPI + Pydantic 已经能提供足够清晰的接口约束,同时保持极低的使用门槛。
在没有明确性能瓶颈前,HTTP 是更"刚刚好"的选择。
四、Python 服务如何发布得"像个正经组件"
Python 算法服务最终会被打包成 algo_service.exe,关键点有几个:
-
PyInstaller 使用
--noconsole- 服务在后台运行,不弹黑窗口
-
禁用 uvicorn 默认 logging
uvicorn.run(..., log_config=None)- 避免
sys.stdout is None导致的isatty()异常
-
日志交给 Qt 管
- Qt 用
QProcess::setStandardOutputFile()重定向日志 - 用户无感知,但问题可排查
- Qt 用
这样对用户来说,算法服务是"隐形的内部组件",不会显得突兀。
五、Qt 侧真的需要多线程吗?
一个容易被误判的点是"要不要上多线程"。
结论其实很简单:
-
启动/停止 Python 服务:不需要线程
QProcess本身就是异步的
-
状态查询(/health):不需要线程
- 用异步 HTTP 即可
-
只有在强行使用"同步 HTTP 调用"时
- 才需要把调用放到工作线程,避免卡 UI
也就是说,不是为了"看起来专业"而用线程,而是为了不阻塞 UI。在这个方案里,多线程是可选而不是必需。
六、为什么最终放弃托盘程序?
一开始也考虑过做一个 Python 托盘程序来管理算法服务,但最终发现:
- 既然已经有 Qt 主程序
- 算法服务只在主程序运行期间需要
- 再加一个托盘进程反而显得"像外挂"
最终选择是:
直接在 Qt 主程序里提供一个"算法服务状态面板",显示 Running / Stopped、版本、运行时间,并提供启动/停止/查看日志的入口。
这在产品体验上更自然,也减少了一个交付物。
七、最终架构总结
最终的整体结构非常清晰:
Qt 主程序
├─ QProcess 管理 algo_service.exe
├─ HTTP 调用 /health /infer /shutdown
└─ UI 显示算法状态与日志入口
algo_service.exe(Python)
├─ FastAPI 提供算法接口
├─ 独立进程,独立生命周期
└─ 可随时替换或升级