ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++嵌入式Python虚拟环境配置实战:从路径到部署全解析

C++嵌入式Python虚拟环境配置实战:从路径到部署全解析 写这篇文章之前我先把场景说清楚C作为宿主程序在进程内部启动一个Python解释器再通过C API去调用Python模块同时要求这个Python解释器用的不是系统全局环境而是一个专门为项目创建的虚拟环境。这个组合在业界叫“嵌入式Python”常用于游戏脚本、量化策略、工业软件、数据分析工具等场景。很多人做这一步时都卡在同一个地方——能用系统Python跑通一换虚拟环境就各种ModuleNotFoundError。这篇就把整个链路从原理到实操完整拆一遍包括虚拟环境怎么建、C侧怎么配置、路径怎么指、坑都在哪。1. C为什么要嵌Python不是炫技是被逼的1.1 嵌入不是“语言绑定”是C当宿主先理清一个概念。pybind11、Boost.Python这类库属于“语言绑定”解决的是“在Python里调用C函数”的问题也就是Python当主语言C作为扩展模块被import。而“嵌入Python”是反过来的——C程序是主进程Python解释器作为库被链接进这个进程C代码主动创建解释器实例然后调用Python脚本里的函数。这两件事经常被混着提但用途完全不同。C嵌入Python最直接的价值在于C负责底层性能、设备交互、内存管理、界面主循环Python负责上层业务逻辑、算法原型、数据处理。Python侧改起来快不需要重新编译C工程这对需要频繁调整策略、规则、脚本的项目来说开发效率提升非常明显。实测下来一个用C写核心逻辑、Python写业务层的项目迭代速度比纯C快两到三倍而且稳定性一点不差。1.2 典型场景哪些项目真正需要这种杂交方案我把这些年见过的实际项目归类一下基本就是下面几个方向量化交易/策略回测引擎C做行情接收、撮合模拟、资金计算Python跑策略逻辑。策略研究员用numpy、pandas写因子计算甚至直接跑机器学习模型C端只要把行情数据喂进去再把Python算出的信号拿出来执行就行。游戏/可视化工具的脚本系统C引擎保留核心渲染与物理运算用Python写关卡逻辑、AI行为、编辑器扩展脚本支持热更新策划不需要等待编译。工业控制与设备自动化C负责串口/网口通信、PLC交互、实时数据采集Python负责上位机里的数据分析、报告生成、参数标定。数据分析与报表工具C做表格解析与核心运算Python用pandas做清洗聚合、matplotlib画图返回结果给C界面显示。这些项目有一个共同特征性能敏感的链路在C侧频繁变化的逻辑在Python侧。如果你手里的项目恰好是这种结构就值得认真考虑嵌入方案。1.3 方案选型嵌入 vs 子进程 vs 插件框架有人可能觉得既然C和Python要协作直接让C启动一个Python子进程不就行了数据走文件或管道不需要折腾嵌入API。这个思路在某些场景可行但代价不小。子进程方案的数据交互要走序列化JSON、pickle、protobuf每传一次数据都有编解码开销而且Python子进程的内存空间和C不共享。如果业务是“C批量把10万个点位传给Python计算Python再把结果返回来”这种高频小粒度的数据交换用子进程的序列化开销会直接吃掉性能收益。嵌入方案则不同C侧的PyObject直接指向Python对象内存传递numpy数组时只是一次内存视图共享零拷贝。插件框架比如把Python编译成独立exeC调它就更重了维护成本高而且热更新能力大打折扣。所以我的建议很简单只要你需要在同一个进程内频繁交换数据就选嵌入如果只是低频传几个字符串子进程也可以接受。2. 虚拟环境嵌入前必须想清楚的依赖管理问题2.1 为什么说“虚拟环境”是嵌入项目的第一个坑有经验的开发者都知道Python的依赖隔离是项目上线前必须处理的头等大事。C嵌入Python时这个问题会放大。原因很直接系统Python环境通常是全局共享的机器上可能有项目A装的pandas 2.x项目B装的pandas 1.x还有一堆pip install时自动拖进来的传递依赖。C程序一旦默认加载系统Python它的sys.path会把全局site-packages全部暴露给脚本。一旦脚本里import pandas导到的版本和你开发时用的不一致轻则行为差异重则直接崩。更严重的是生产部署机上如果连某个基础库都没装C程序启动后就是一句ModuleNotFoundError用户根本不知道去哪装。虚拟环境的价值就在于制造一个“封闭的Python世界”。在虚拟环境里安装的包只对这个环境可见C程序启动解释器时只要把路径指到这个虚拟环境就能保证使用的依赖和开发环境完全一致。这本质上和C里用静态库跑一个道理把环境依赖打包带走避免运行时“缺斤少两”。2.2 venv与conda两个主流方案怎么选迁移D盘怎么处理虚拟环境的实现主要有两个流派官方venv和conda。我在嵌入项目里两种都折腾过总结一下它们的差异。venv轻量、干净只解决包隔离问题不解决解释器版本问题。也就是说你本机装的是Python 3.10那用venv建出来的环境也只能是Python 3.10只是包是独立的。好处是体积小、标准库自带、不需要额外装工具坏处是如果你的项目要跑在其他机器上而那台机器压根没装Python 3.10venv环境基本没法直接用。conda或者更轻量的Miniconda则能把整个Python解释器连同第三方库都管理起来。它创建的虚拟环境里有完整的python.exe、独立的Lib/site-packages还支持指定Python版本比如项目用Python 3.8conda直接建一个3.8环境未来迁移部署就灵活得多。热词里有个高频问题“conda虚拟环境怎么迁移到D盘”这也是我实际遇到的。conda环境默认放在C:\Users\用户名\anaconda3\envs\下C盘空间紧张时确实很麻烦。我的处理方式是创建时就指定路径conda create -p D:\envs\myproject python3.12 numpy pandas -y这样环境就直接落在D盘。如果已经建在默认位置用复制的方式迁移也可以但要记得把环境里的脚本路径修正一下否则有些基于路径生成的配置会失效。最省事的做法是用conda env export导出环境配置然后在目标机器指定新路径重建conda env export -n myproject environment.yaml # 然后在D盘重建 conda env create -f environment.yaml -p D:\envs\myproject这样环境里所有包版本都能锁死不会出迁移后版本漂移问题。need这里补充一点嵌入项目建议优先用conda创建环境作为嵌入目标。因为conda环境是“自包含的Python环境”有完整的解释器文件结构C侧指向它时逻辑更干净venv环境的site-packages虽然在环境目录下但标准库仍然依赖base解释器指向时容易绕弯子。后面第4节会详细讲路径配置这里先把选型定下来。3. 从零开始C嵌入Python的核心API与初始化流程3.1 从include到Py_Initialize到底发生了什么C嵌入Python第一步是引入Python解释器的头文件和库。Windows下用官方安装器装完Python后安装目录里会有include和libs两个文件夹里面分别是Python.h和python312.lib版本号随你安装的Python版本走。在Visual Studio的项目属性里VC目录 - 包含目录填D:\Python312\includeVC目录 - 库目录填D:\Python312\libs链接器 - 输入 - 附加依赖项添加python312.libLinux下则是sudo apt install python3-dev python3-venv编译时加-I /usr/include/python3.12 -l python3.12。代码层面需要在所有C标准库头文件之前先包含Python.h或者至少在第一个#include之前包含它原因在官方文档里写得很清楚Python.h里定义了一些会影响其他头文件的宏顺序不对会导致编译错误。初始化解释器最基础的套路是#include Python.h int main() { Py_Initialize(); PyRun_SimpleString(print(python ok)); Py_Finalize(); return 0; }Py_Initialize()做的事情比你看到的要多得多它会初始化解释器核心状态、加载site模块、根据编译时的配置信息设置sys.path、初始化GIL等等。这一步没做好后面所有操作都会出问题。3.2 模块导入、函数调用与引用计数三板斧初始化之后真正的业务调用是围绕三个对象展开的模块对象、函数对象、参数/返回值对象。// 导入Python模块 PyObject* pModule PyImport_ImportModule(my_script); if (!pModule) { PyErr_Print(); // 打印Python侧的错误堆栈 return -1; } // 获取模块里的函数 PyObject* pFunc PyObject_GetAttrString(pModule, compute); if (!pFunc || !PyCallable_Check(pFunc)) { PyErr_Print(); return -1; } // 构造参数并调用格式字符串里 i 表示ints 表示const char* PyObject* pArgs Py_BuildValue((is), 42, hello); PyObject* pResult PyObject_CallObject(pFunc, pArgs); // 解析返回值 int result 0; PyArg_ParseTuple(pResult, i, result);这里最容易被忽略的是引用计数。Python的C API里PyImport_ImportModule、PyObject_GetAttrString、Py_BuildValue、PyObject_CallObject返回的都是“新引用”使用完毕后必须调用Py_DECREF释放否则内存会一直涨。Py_DECREF(pArgs); Py_DECREF(pFunc); Py_DECREF(pModule); Py_DECREF(pResult);我见过不少项目功能跑通了但内存曲线像爬坡进程跑一晚上内存就翻倍最后定位就是漏了Py_DECREF。这个没法靠编译器发现只能靠代码审查和内存检测工具排查千万别偷懒。3.3 Python 3.8之后的新初始化方式如果你用的是Python 3.8及以上版本官方推荐使用PyConfig结构体来做初始化而不是老的Py_SetPythonHome之类散装函数。新版API的好处是配置项集中、生命周期清晰、对多阶段初始化有更好的支持。PyStatus status; PyConfig config; PyConfig_InitPythonConfig(config); // 设置解释器路径 std::wstring home LD:\\work\\myproj\\venv; PyConfig_SetString(config, config.home, home.c_str()); status Py_InitializeFromConfig(config); if (PyStatus_Exception(status)) { Py_ExitStatusException(status); } PyConfig_Clear(config);需要注意PyConfig_SetString要求传入宽字符串因为Python内部路径处理是宽字符的中文路径、带空格路径在这里都不会出问题。这一点在Windows上格外重要很多从新手版教程抄来的Py_SetPythonHome接窄字符串的方案遇到中文路径就各种乱码崩溃。3.4 多线程场景的GIL处理嵌入式方案里多线程是躲不开的话题。C程序的主线程初始化了Python解释器但如果还有工作线程要调用Python函数你必须先拿到GIL全局解释器锁。规则很简单只有初始化了Python解释器的线程通常是主线程才能直接调用Python API。工作线程调用Python API前必须PyGILState_Ensure()用完再PyGILState_Release()。如果C主线程在等待其他线程的结果应该先PyEval_SaveThread()释放GIL等结果回来后再PyEval_RestoreThread()重新获取避免阻塞其他线程的Python调用。// 工作线程里的调用模板 void workerThread() { PyGILState_STATE gstate PyGILState_Ensure(); PyObject* result PyObject_CallObject(pFunc, pArgs); Py_DECREF(result); PyGILState_Release(gstate); }这块的坑在于如果用错了锁的管理方式轻则死锁重则解释器直接崩。我的建议是在C侧封装一个Python调用管理器所有跨线程的Python调用都走同一个入口统一做好GIL的获取和释放不要在业务代码里到处穿插Py API调用。4. 让C认识虚拟环境那几条关键路径不能配错4.1 嵌入后默认加载的Python环境不是你想要的很多人在C嵌入Python时遇到的第一个“灵异事件”是明明命令行里用虚拟环境的Python跑脚本没问题但C程序里import numpy就报错。原因很简单——C默认初始化出来的解释器用的是编译链接时指定的Python这个Python的正常模块搜索路径是它自己安装目录下的site-packages和虚拟环境半毛钱关系都没有。打个比方你C程序相当于一个公交车司机虚拟环境里的Python是一个独立运营的出租车队。公交车默认只会开到自己的总站系统Python目录根本不知道出租车队在哪。想让公交车去出租车队拉人你得把路线指过去而且指对站点。4.2 将虚拟环境目录设为核心Python路径前面3.3里提到了PyConfig.home这就是最关键的“路线”。设置路径时我建议把home直接指向虚拟环境目录本身。原因在于虚拟环境目录里存在一个pyvenv.cfg文件Python解释器初始化时会读取这个文件识别当前环境是venv然后自动调整前缀路径、模块搜索路径。也就是说如果你把config.home指到venv目录Python启动后会自己明白“我是虚拟环境”从而把sys.path首位设置为虚拟环境的site-packages。如果用的是conda创建的环境做法类似home指向环境目录比如D:\envs\myproject。conda环境结构更“完整”指向后Python解释器会直接把它当作自己的前缀路径。4.3 动态修改sys.path的兜底方案光设home还不够稳妥因为有些情况下home只影响标准库路径site-packages的添加时机和方式随版本有细微差异。我通常在初始化完成后还会做一次主动的sys.path修正void PrependToSysPath(const std::wstring path) { PyObject* sysPath PySys_GetObject(path); if (!sysPath || !PyList_Check(sysPath)) return; PyObject* pathObj PyUnicode_FromWideChar(path.c_str(), (Py_ssize_t)path.size()); PyList_Insert(sysPath, 0, pathObj); Py_DECREF(pathObj); } int main() { // 前面的初始化省略... PrependToSysPath(LD:\\work\\myproj\\venv\\Lib\\site-packages); // 再执行业务代码 }这段代码的原理很简单sys.path决定了import语句搜索模块的顺序把它插到第一位虚拟环境里的包的优先级就是最高不会和系统Python环境的包产生歧义。算是给config.home上了一道双保险。4.4 版本一致性与相对路径的工程化建议这里有个关键约束嵌入用的Python解释器版本必须与虚拟环境的解释器版本一致。举个例子你的C工程链接的是python312.lib那虚拟环境就应该基于Python 3.12创建。如果你链接3.12但虚拟环境是3.8的把3.8环境的路径塞进3.12解释器里最轻的是模块加载失败严重时直接崩溃。这个版本一致性检查我在项目启动代码里加了显式判断用Py_GetVersion()拿解释器版本再和配置文件里的预期版本比对不一致就打日志终止启动避免线上事故。另外如果用绝对路径C程序换台机器部署路径就得改。我建议在C程序启动时优先查找自身exe所在目录下的.venv文件夹wchar_t exePath[MAX_PATH] {0}; GetModuleFileNameW(NULL, exePath, MAX_PATH); std::filesystem::path exeDir(exePath).parent_path(); std::filesystem::path venvDir exeDir / .venv; if (std::filesystem::exists(venvDir / pyvenv.cfg)) { // 找到了随程序分发的虚拟环境 config.home venvDir.c_str(); }这样发布时把虚拟环境整个目录拷到exe旁边程序就能自举用户不需要手动配置任何Python路径。这是我认为嵌入场景最省心智的部署方式。5. 构建配置与多平台落地细节5.1 Visual Studio与CMake的完整配置写法我平时主要用CMake管理这类混合工程find_package的现代写法比手动配VS参数要省心得多cmake_minimum_required(VERSION 3.16) project(embed_python_demo) find_package(Python 3.12 REQUIRED COMPONENTS Interpreter Development) add_executable(embed_demo main.cpp) target_include_directories(embed_demo PRIVATE ${Python_INCLUDE_DIRS}) target_link_libraries(embed_demo PRIVATE ${Python_LIBRARIES})这里的关键点是find_package(Python ... COMPONENTS Development)它会自动找到Python.h和对应的库文件。如果你系统里装了好几个Python版本可以用Python_EXECUTABLE变量指定路径set(Python_EXECUTABLE D:/Python312/python.exe)如果用Visual Studio直接建工程就按3.1节里说的在项目属性里把include目录、lib目录、附加依赖项逐项填好别漏了链接库那一步这是新手最常见的错误。5.2 x64/x86架构、Debug/Release必须对齐架构不匹配是一个碰运气式的问题。机器是64位的你装了64位Python但VS工程不小心编了个x86版本运行时会直接报“无法加载DLL”因为32位进程加载不了64位的python312.dll。反过来同理。这种错误提示往往很笼统排查半天才反应过来是架构问题。解决办法很简单把VS的“解决方案平台”和“链接的Python库架构”统一成同一个。官方Python安装器默认装64位那工程就选x64。如果必须用32位就单独装一个32位Python链接时确保lib和解释器都来自同一个32位安装。Debug/Release的问题则更隐蔽。官方Python安装目录下的libs文件夹里有两个文件python312.lib和python312_d.lib前者对应Release版解释器后者对应Debug版。如果C工程用的是Debug编译却链了python312.lib链接器会挂一个LNK4098的兼容性警告运行阶段各种诡异内存问题。正确做法是Debug工程链python312_d.libRelease链python312.lib且对应模式下的解释器DLL会从不同路径加载。5.3 运行时DLL的发布策略C程序编出来后要跑在别的机器上除了业务DLL外还需要Python解释器的核心DLL也就是python312.dll。这文件默认在Python安装目录里不会自动跟着exe走。发布时有几个选择把python312.dll连同exe放在同一目录最简单但也最原始。把整个虚拟环境目录随程序分发环境里自带解释器文件exe启动时动态找到它。用PyInstaller把Python侧逻辑打包成独立的包再嵌入但这样就和虚拟环境的思路偏离了适用于无Python运行时的极端场景。我个人推荐第二种因为它保留了脚本的热更新能力改Python文件用户重启程序就生效不需要重新编译C也不需要重装环境自动化运维非常方便。有个小细节值得记一下发布时环境里可能会有开发机的绝对路径残留比如某些包里的.pth文件记录了安装路径。分发前用干净的conda环境从零conda env create -f environment.yaml重建一次或者用venv pip install -r requirements.txt重装能避开这类路径硬编码问题。6. 踩坑实录我在嵌入项目里遇到的高频问题6.1 Python.h找不到、链接失败、DLL加载不了这三个问题大概率能归到三类原因第一Python安装了但没装开发组件。官方Windows安装器有可选组件面板默认是“仅运行时”只有勾选“Python开发环境和调试符号”之类的组件include和libs目录才会生成。解决办法是重跑安装器选择Modify把开发组件勾上。第二CMake的find_package找不到Python。版本缓存问题用Python_EXECUTABLE显式指定可解。有时候是因为环境变量PATH里没有Python目录导致Interpret组件检测失败。第三启动时提示找不到python312.dll。把Python安装目录加到PATH或者把DLL复制到exe旁边。建议后者因为上线机器不会给你乱改环境变量权限。6.2 ModuleNotFoundError路径指向了但模块还是找不到这个是最常见的运行期问题。我排查的顺序是先看PyConfig.home指向的路径是否存在目录名有没有拼错。打印运行时的sys.path确认虚拟环境的site-packages在不在列表里。确认虚拟环境的Python版本和嵌入链接的Python版本一致。检查虚拟环境里的包是不是真的装上了有时候conda环境激活时能import成功但C程序用的完全是另一个环境的路径看似一样实则南辕北辙。我通常会在初始化后打印一次sys.path放在日志里线上排查这个问题的效率能提升80%。6.3 解释器崩溃引用计数和GIL的老问题崩溃问题九成出在引用计数和GIL上。引用计数的使用失误主要在于Py_DECREF调用次数多于实际引用次数导致解释器回收了还在使用的对象或者忘了释放导致内存泄漏。我的建议是封装统一的PyObject智能指针类作用域结束时自动Py_DECREF。C侧有一个现成的pybind11::object可以省心但如果不想引入额外依赖自己写一个几十行的RAII包装类也非常值得。至于GIL反复强调一遍C工作线程里调用任何Python API之前先PyGILState_Ensure()主线程如果在等待其他线程的Python调用结果应该先PyEval_SaveThread放锁程序退出时确保所有子线程退出后再调用Py_Finalize。顺序错了就是死锁或者崩溃。6.4 中文路径与编码问题Windows上如果项目路径、虚拟环境路径里有中文老方案Py_SetPythonHome(const char*)很容易在内部编码转换时出问题。解决办法是用PyConfig系列API的宽字符串版本或者统一使用std::wstring传路径。另外Python脚本文件本身如果有中文注释或者中文字符串字面量最好在文件头显式声明# -*- coding: utf-8 -*-避免解释器用默认编码解析源码时崩掉。6.5 日志与错误处理的调试技巧最后分享一个我在实际项目中用起来很顺的调试方式。嵌入Python时Python侧的错误默认不会显示在C的控制台里经常是C端拿到了一个NULL返回值就干瞪眼。解决方法是初始化后设置一个错误输出钩子// 在Py_Initialize之后调用 PyRun_SimpleString(import sys; sys.stderr open(rD:/logs/py_err.log, w, encodingutf-8));这样Python侧的任何错误堆栈都会写入日志文件C这边只要检查返回值是否为NULL配合日志里的Python堆栈问题定位就非常快了。上线环境里我一般还会对接一个成熟的日志库把Python侧的关键print输出也重定向到同一个日志文件里排查问题时能同时看到C和Python两边的上下文。7. 一个可直接运行的完整示例C调用虚拟环境里的Python做数据处理聊了这么多原理和坑最后放一个我手头整理过的完整最小示例覆盖从虚拟环境创建到C调用再到结果解析的全过程。先创建虚拟环境这里用condaconda create -p D:\work\demo\pyenv python3.12 numpy pandas -y然后写一个Python模块calc.py放到工程目录下import numpy as np def moving_average(data, window): arr np.asarray(data, dtypenp.float64) if window 0: raise ValueError(window must be positive) kernel np.ones(window) / window return np.convolve(arr, kernel, modevalid).tolist()C侧的主程序main.cpp#include Python.h #include iostream #include filesystem void PrependToSysPath(const std::wstring path) { PyObject* sysPath PySys_GetObject(path); PyObject* pathObj PyUnicode_FromWideChar(path.c_str(), (Py_ssize_t)path.size()); PyList_Insert(sysPath, 0, pathObj); Py_DECREF(pathObj); } int main() { PyConfig config; PyConfig_InitPythonConfig(config); // 假设虚拟环境在当前工程目录下的 pyenv 文件夹 std::filesystem::path venvPath std::filesystem::current_path() / pyenv; PyConfig_SetString(config, config.home, venvPath.c_str()); PyStatus status Py_InitializeFromConfig(config); if (PyStatus_Exception(status)) { Py_ExitStatusException(status); } PyConfig_Clear(config); // 双重保险把虚拟环境的site-packages插到sys.path首位 std::filesystem::path sitePkg venvPath / Lib / site-packages; PrependToSysPath(sitePkg.wstring()); PyObject* pModule PyImport_ImportModule(calc); if (!pModule) { PyErr_Print(); Py_Finalize(); return -1; } PyObject* pFunc PyObject_GetAttrString(pModule, moving_average); if (!pFunc || !PyCallable_Check(pFunc)) { PyErr_Print(); Py_DECREF(pModule); Py_Finalize(); return -1; } // 模拟一批采样数据 std::vectordouble data {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0}; int window 3; PyObject* pList PyList_New((Py_ssize_t)data.size()); for (size_t i 0; i data.size(); i) { PyList_SetItem(pList, (Py_ssize_t)i, PyFloat_FromDouble(data[i])); } PyObject* pArgs Py_BuildValue((Oi), pList, window); PyObject* pResult PyObject_CallObject(pFunc, pArgs); Py_DECREF(pList); Py_DECREF(pArgs); Py_DECREF(pFunc); Py_DECREF(pModule); if (!pResult) { PyErr_Print(); Py_Finalize(); return -1; } // 打印Python返回的移动平均结果 Py_ssize_t n PyList_Size(pResult); std::cout moving average result: ; for (Py_ssize_t i 0; i n; i) { PyObject* item PyList_GetItem(pResult, i); std::cout PyFloat_AsDouble(item) ; } std::cout std::endl; Py_DECREF(pResult); Py_Finalize(); return 0; }这个示例已经把主要的环节都覆盖了虚拟环境的定位、sys.path修正、模块导入、参数构造、函数调用、返回值解析和引用释放。把它跑通你就能在这个骨架上扩展出自己的业务。我在实际试这个示例时发现一个容易忽略的细节Py_BuildValue的格式字符串(Oi)里O表示以PyObject*传入但传入的pList不需要额外增加引用计数因为Py_BuildValue会自己接管这个引用。也就是pList在Py_BuildValue返回后仍然只有你手动创建时的那一个引用所以Py_DECREF(pList)一次就够了。如果这里多释放一次会在程序结束时再次释放同一对象触发引用计数错误甚至段错误。这种细节文档里不会特意标注只能在实践中积累。如果你想在C侧频繁传递大量数值数组建议把数据类型升级为numpy的PyArrayObject通过PyArray_SimpleNewFromData包装C内存指针这样numpy内部操作零拷贝效率会高出一个数量级。不过这块依赖numpy的C API需要在编译时加入numpy的头文件路径属于进阶玩法等基础流程跑通后再上手也不迟。我在实际项目里的体会是C嵌入Python这个方案初期搭建环境时确实有一定门槛但一旦把虚拟环境路径、版本对齐、依赖管理这几件事理顺后期的开发和维护体验是相当顺畅的。尤其是“C逻辑不动Python侧随便改”这种迭代节奏能让团队里的算法同学和工程同学各司其职互不阻塞。最后再分享一个小技巧把虚拟环境的创建和依赖安装做成一个自动化脚本部署时一键执行线上和本地的环境就不会出现细微偏差这类看似不起眼的细节往往是项目长期稳定运行的关键。
返回列表