ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C# × Python 互操不再难:DotNetPy 让两大生态真正融合

C# × Python 互操不再难:DotNetPy 让两大生态真正融合 在好些后端项目当中, 会碰到这样一种场景, 系统主体是用C#编写的, 工程化程度相当高, 然而有一部分AI推理或者数据分析的逻辑, 偏偏唯有特定生态方可解决, 于是开发者们开始误入歧途, 有人启动一个子进程去运行特别脚本, 有人封装一层Web API进行中转, 还有人进行尝试, 结果发觉NumPy根本无法运行。那些方案, 不是性能差得出奇, 就是维护成本高得令人头疼, 进程启动开销动不动就几百毫秒, 序列化反序列化耗费大量 CPU, 而且状态没办法跨调用持久化。今日所谈及的, 其读音为 dot-net-pie, 是一款专门针对现代.NET 予以设计的轻量级互操作库, 该库支持 AOT, 具备内置 uv 声明式依赖管理, 只需 4 行代码便能够在 C# 之中运行。在读完此文之后, 你将会掌握三种能够直接予以落地实施的方法: 能够实现快速接入, 可达成数据双向传递, 以及进行声明式环境管理。问题到底出在哪里咱们先把痛点说清楚不然后面的方案就没有对比价值。传统方案一: .Start 去调用脚本, 这属于最为常见的做法, 看上去简单又粗暴, 事实上每一次调用都得创建一个全新的进程, 冷启动的时间处于 200ms 至 500ms 之间, 并且每个进程还都要占用 50MB 以上的内存。要是你的服务每秒需要处理几百次请求, 那么这条路基本上就行不通, QPS 的天花板不会超过 100。包一层 HTTP/gRPC 接口, 这是传统方案二, 它呢, 引入了网络延迟, 还有序列化开销, 两个进程之间的数据传递走 JSON 或者, 复杂对象的转换成本可不低。更麻烦的在于, 服务需要单独部署, 还要单独运维, CI/CD 流水线的复杂度直接就翻倍了。传统方案三, 理论上最为优雅, 它直接在.NET 进程内运行。但存在的问题是, 重新以 C#实现来构建进行解释, 却无法加载用 C 编写的扩展库, 这就导致 NumPy 等全都无法使用, 就此而言, 当处于 AI 时代时, 这几乎等同于将那最具价值的一半生态给废掉了。传统方案四: .NET。它具备强大功能, 支持双向互操作, 然而其架构相当重, 配置繁杂, 在AOT场景当中存有兼容性问题, 对于追求极致部署体积的云原生应用而言并不友善。的核心设计思路有着一条不一样的路径, 是直接进行封装的原生C API, 借助.NET的P/技术去朝着底层解释器作出交谈, 可不是在那一层上方再次构筑一层抽象哦。此究竟意味着啥呢? 意味着它跟 属于同一个运行时, 所有的C扩展库像是NumPy、、均可正常予以使用, 性能方面的损耗极其微小, 数据传递所经由的是内存地址引用而非序列化状态。实际测试对比得出的数据情况如下测试所依托的环境为: .NET 8, 3.11, i7-, 11:进程之内的调用, 将操作系统的上下文切换予以去除, 状态能够跨越调用而持续保存, 模块仅仅需要加载一回, 后续的调用直接进行重复使用。另外一个值得提及的亮点在于, 存在着AOT支持。众多传统的互操作库依靠运行时反射以及动态IL生成, 然而却无法借助AOT编译器的剪裁分析。借着通过静态封装C API这一方式, 完全把这个限制给规避掉了, 从而发布的二进制文件能够在没有.NET运行时的机器上直接进行运行。方案一快速接入4 行代码跑起来 安装与初始化首先通过 .NET CLI 添加 NuGet 包dotnet add package DotNetPy然后在 .cs 中初始化解释器using DotNetPy; // 自动探测系统中已安装的 Python无需手动指定路径 Python.Initialize; // 获取执行器实例后续所有操作的入口 var executor Python.GetInstance;在自动扫描系统这儿, 对于动态链接库, 其当下的模样在这儿边是.dll 情形的, 而当置身于Linux语境里的时候呢, 它呈现为.X.so 样子, 会依据架构方向去匹配, 架构方面会有x64以及Arm64类别, 从而找出最为适配的版本。执行简单表达式using DotNetPy; namespaceAppDotNetPy { internalclassProgram { staticasync Task Main(string[] args) { // 自动探测系统中已安装的 Python无需手动指定路径 Python.Initialize; // 获取执行器实例后续所有操作的入口 var executor Python.GetInstance; // Evaluate 用于计算表达式并返回值 var result executor.Evaluate(list(range(1, 6)))?.ToList; if (result ! ) { foreach (var item in result) Console.WriteLine(item); // 输出 1 到 5 } // Execute 用于执行不返回值的代码片段 executor.Execute( import os print(f当前工作目录: {os.getcwd}) ); } } }仅这些而已, 无需进行配置路径的操作, 无需书写P/, 无需去关心GIL, 其在内部就自动对GIL实现了申请以及释放的工作, 使开发者全然无法察觉到它的存在。进行踩坑告知, 需注意, 在整个进程的生命周期范围以内, 仅能够调用一次, 在此建议将其放置于 DI 容器的单例注册之处, 或者 Main 方法的最顶部位置。要是出现重复初始化的情况, 便会抛出运行时异常。方案二C# 与 双向数据传递在实际项目当中, 最为常用的模式并非是“执行一段代码”, 而是“将C#的数据交予处理, 随后再把结果取回来”, 的方法是专门针对这个场景所设计的。数据注入与结果捕获// 准备 C# 侧的输入数据 var inputData newdouble { 1.5, 2.5, 3.5, 4.5, 5.5 }; // using 声明确保 Python 对象引用被及时释放 usingvar capture executor.ExecuteAndCapture( import math # input_data 由 C# 自动注入到 Python 作用域 total sum(input_data) std_dev (sum((x - total/len(input_data))**2 for x in input_data) / len(input_data)) ** 0.5 # 按约定将结果存入 result 变量 result { sum: total, average: total / len(input_data), std_dev: std_dev, max: max(input_data) } , new Dictionary { { input_data, inputData } }); if (capture ! ) { Console.WriteLine(#34;总和: {capture.GetDouble(sum)}); Console.WriteLine(#34;平均值: {capture.GetDouble(average)}); Console.WriteLine(#34;标准差: {capture.GetDouble(std_dev):F4}); Console.WriteLine(#34;最大值: {capture.GetDouble(max)}); }调用 NumPy 进行科学计算这是 相比 最大的优势所在——C 扩展库可以正常使用// 矩阵运算示例C# 传入矩阵数据Python 用 NumPy 处理 var matrixData newdouble { newdouble { 1, 2 }, newdouble { 3, 4 } }; usingvar capture executor.ExecuteAndCapture( import numpy as np # 将 C# 的二维数组转为 NumPy 矩阵 matrix np.array(matrix_data) result { determinant: float(np.linalg.det(matrix)), eigenvalues: np.linalg.eigvals(matrix).tolist, inverse: np.linalg.inv(matrix).tolist } , new Dictionary { { matrix_data, matrixData } }); if (capture ! ) { var det capture.GetDouble(determinant); Console.WriteLine(#34;行列式: {det}); }一种踩坑预警出现了, 即对象持有对于堆内存的引用, 此时必须使用using块或者手动去进行调用, 要是将其释放这件事给忘记了, 那就会致使对象在内存里不断堆积, 而长时间运行的服务就会出现内存泄漏这种情况, 这可是跨语言互操作当中最为常见的坑, 务必要养成相应的习惯。方案三声明式 环境管理置于不少我所历经期间的项目当中, “环境配置”实则堪称十足的噩梦。于开发机之上能够运行, 然而于测试服务器处却出现报错情况业已于镜像之中进行装载, 不过版本并不契合在CI/CD流水线里pip极为缓慢, 时而还会由于网络的问题而趋于失败。把uv集成(uv是一个由Rust撰写的具备高性能的包管理器, 其安装速度相较于pip快10至100倍), 借助把 环境的整个生命周期归入.NET代码管理。声明式环境配置using DotNetPy; using DotNetPy.Uv; namespaceAppDotNetPy { internalclassProgram { staticasync Task Main(string[] args) { // 用流畅 API 声明 Python 环境需求 usingvar project new PythonProjectBuilder .WithProjectName(ai-inference-service) .WithPythonVersion(3.10) .AddDependencies(numpy1.24, pandas2.0, scikit-learn1.3) .Build; // 异步初始化创建虚拟环境、安装依赖 await project.InitializeAsync; Console.WriteLine(#34;Python 环境目录: {project.WorkingDirectory}); Console.WriteLine(#34;Python 可执行文件: {project.PythonExecutable}); Console.WriteLine(#34;Python 库文件: {project.PythonLibrary}); // 推荐方式直接获取该 uv 项目的 DotNetPy executor var executor project.GetExecutor; Console.WriteLine(Python 环境就绪开始推理...); executor.Execute( import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression print(numpy:, np.__version__) print(pandas:, pd.__version__) ); } } }这段代码于首次运行之际, 会自行达成全部配置, 后续运行若检测到环境已然存续, 便会径直跳过。整个流程并不依存于开发机的预装状况, 在CI/CD流水线当中同样能够稳定重现。隔离执行器多租户与并行推理在存在需要进行并发处理的情形下, 像是多租户系统或者并行ML推理这类情况, 给出了相应方法, 每一个执行器都具备独立的命名空间。using DotNetPy; using DotNetPy.Uv; namespaceAppDotNetPy { internalclassProgram { staticasync Task Main(string[] args) { Python.Initialize; // 为每个租户创建独立的 Python 执行环境 var tenant1Executor Python.CreateIsolated; var tenant2Executor Python.CreateIsolated; // 两个执行器互不干扰变量作用域完全隔离 await Task.WhenAll( Task.Run( { usingvar capture tenant1Executor.ExecuteAndCapture( result {tenant: A, score: 0.95}); Console.WriteLine(#34;租户A结果: {capture?.GetString(tenant)}); Console.WriteLine(#34;租户A得分: {capture?.GetDouble(score)}); }), Task.Run( { usingvar capture tenant2Executor.ExecuteAndCapture( result {tenant: B, score: 0.87}); Console.WriteLine(#34;租户B结果: {capture?.GetString(tenant)}); Console.WriteLine(#34;租户B得分: {capture?.GetDouble(score)}); }) ); Console.ReadKey; } } }踩坑预警, 安全方面得格外留意, 执行的是字符串样式的代码, 万万不可把用户输入直接拼接进去, 心怀恶意的用户没准会透过注入os.(‘...’)运行任意的系统命令,内置了静态分析器, 其能够在编译阶段查探出潜在的注入风险, 建议将其开启。
返回列表