C#/.net程序调用python
C#/.net 程序调用 python
C#的优势在于 window 下的开发,不仅功能强大而且开发周期短。而 python 则有众多的第三方库,可以避免自己造轮子,利用 C# 来做界面,而具体实现使用 python 来实现可以大大提高开发效率。本文介绍如何使用pythonnet
来执行 python 脚本,使用pythonnet
既可以具有较高的交互性,又可以使用第三方 python 库,同时可以将程序需要的 python 环境及第三方库打包到软件中,避免用户进行 python 的环境配置。
C# 调用 python 的常见方法
调用 python 常见的方法有 4 种
方式 | 优点 | 缺点 |
---|---|---|
使用 IronPython | 无需安装 python 运行环境,交互性强,C# 和 python 无缝连接 | 某些 python 第三方库不支持,如 numpy |
使用 C++ 调用 Python,然后将 C++ 程序做成动态链接库 | 交互性较强 | 需要用户配置 Python 环境,实现方式复杂 |
利用 C# 命令行调用 py 文件 | 执行速度快 | 需要用户配置 Python 环境,交互性差 |
将 python 文件打包成 exe 进行调用 | 无需安装 python 运行环境, | 执行速度慢,传递数据复杂,交互性差 |
可以看出 4 种方式均有限制,很难同时满足交互性强、可调用第三方 python 库、无需用户配置 Python 环境要求,而这几项要求恰恰是一款成熟软件所必须的。而使用pythonnet
库可满足以上三点要求。
本文均在.net 6 环境下测试
使用 pythonnet
-
Nuget 安装
pythonnet
-
设置
Runtime.PythonDLL
属性,即 pythonxx.dll 路径,xx 为版本号 -
设置
PythonEngine.PythonHome
,即 python.exe 所在路径 -
设置
PythonEngine.PythonPath
,python 脚本所在目录,可以放置多个路径,以分号隔开,但是 pathToVirtualEnv\Lib\site-packages 和 pathToVirtualEnv\Lib 应放在最后 -
调用
PythonEngine.Initialize();
string pathToVirtualEnv = ".\\envs\\pythonnetTest"; Runtime.PythonDLL = Path.Combine(pathToVirtualEnv, "python39.dll"); PythonEngine.PythonHome = Path.Combine(pathToVirtualEnv, "python.exe"); PythonEngine.PythonPath = $"{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib"; PythonEngine.Initialize(); // 调用无参无返回值方法 using (Py.GIL()) // 执行 python 的调用应该放在 using (Py.GIL()) 块内 { //python 对象应声明为 dynamic 类型 dynamic np = Py.Import("test"); np.hello(); } // 调用有参有返回值方法 using (Py.GIL()) { dynamic np = Py.Import("test"); int r = np.add(1, 2); Console.WriteLine($" 计算结果{r}"); }
python 文件,必须放在PythonEngine.PythonPath
设定的目录下
def hello():
print("hello")
def add(a,b):
return a+b
嵌入 Python 环境及使用第三方库
程序中包含 Python 脚本所需要的所有环境以及第三方库可以免去用户的自定义配置。本文使用 Anaconda 来构建专用的虚拟环境。
-
创建专用虚拟环境(windows 下首先切换到要建立虚拟环境的根目录下),执行
conda create --prefix=F:\condaenv\env_name python=3.7
路径及 python 版本根据需要自定义。 -
使用 Anaconda Prompt,激活虚拟环境
conda activate F:\condaenv\env_name
-
本次测试第三方库 Numpy(如果需要其他库,安装方法相同),安装 Numpy
pip install numpy
string pathToVirtualEnv = ".\\envs\\pythonnetTest"; Runtime.PythonDLL = Path.Combine(pathToVirtualEnv, "python39.dll"); PythonEngine.PythonHome = Path.Combine(pathToVirtualEnv, "python.exe"); PythonEngine.PythonPath = $"{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib"; PythonEngine.Initialize() // 使用第三方库 using (Py.GIL()) { dynamic np = Py.Import("numpy"); Console.WriteLine(np.cos(np.pi * 2));
<span class="hljs-built_in">dynamic</span> sin = np.sin; Console.WriteLine(sin(<span class="hljs-number">5</span>)); <span class="hljs-built_in">double</span> c = (<span class="hljs-built_in">double</span>)(np.cos(<span class="hljs-number">5</span>) + sin(<span class="hljs-number">5</span>)); Console.WriteLine(c); <span class="hljs-built_in">dynamic</span> a = np.array(<span class="hljs-keyword">new</span> List<<span class="hljs-built_in">float</span>> { <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span> }); Console.WriteLine(a.dtype); <span class="hljs-built_in">dynamic</span> b = np.array(<span class="hljs-keyword">new</span> List<<span class="hljs-built_in">float</span>> { <span class="hljs-number">6</span>, <span class="hljs-number">5</span>, <span class="hljs-number">4</span> }, dtype: np.int32); Console.WriteLine(b.dtype); Console.WriteLine(a * b); Console.ReadKey();
}
注意:C#和python对象进行数学运算时,必须将Python对象放到前面,例如np.pi*2,不能是2*np.pi
传递对象
可以将 C# 对象传递到 python 中
在 C# 中定义对象
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
<span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> FirstName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> LastName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
string pathToVirtualEnv = ".\\envs\\pythonnetTest";
Runtime.PythonDLL = Path.Combine(pathToVirtualEnv, "python39.dll");
PythonEngine.PythonHome = Path.Combine(pathToVirtualEnv, "python.exe");
PythonEngine.PythonPath = $"{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib";
PythonEngine.Initialize();
// 将 C# 中定义的类型传入 python
using (Py.GIL())
{
Person p = new Person("John", "Smith");
PyObject pyPerson = p.ToPython();
string r1 = test.FullName(pyPerson);
Console.WriteLine($" 全名:{r1}");
}
python 脚本
def FullName(p):
return p.FirstName+""+p.LastName
调用 pyd 文件
pyd 文件主要有以下 2 点作用:
- 安全性更高:通过 pyd 生成的文件,已变成了 dll 文件,无法查看源码
- 编译成 pyd 后,性能会有提升
将.py 文件编译成 pyd 文件步骤如下:
pip install cython
- 在.py 文件目录下创建 setup.py 文件
from distutils.core import setup
from Cython.Build import cythonize
setup(
name = "testName",
ext_modules = cythonize("test.py"), #将 test.py 文件编译成 pyd
)
- 执行编译命令
python setup.py build_ext --inplace
最后生成的 pyd 文件一般是 test+cpython 版本 - 平台为文件名,可以重命名为 test 名称,也可以不管,使用时仍然可以按 test 调用。
调动 pyd 文件和调用 py 文件相同,但是执行效率大大增强,下文会对执行速度进行对比。
执行速度对比
在 test.py 中定义一个耗时函数
import time
def Count():
start = time.perf_counter()
<span class="hljs-built_in">sum</span> = <span class="hljs-number">0</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10000</span>):
<span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10000</span>):
<span class="hljs-built_in">sum</span> = <span class="hljs-built_in">sum</span> + i + j
<span class="hljs-built_in">print</span>(<span class="hljs-string">"sum = "</span>, <span class="hljs-built_in">sum</span>)
end = time.perf_counter()
runTime = end - start
runTime_ms = runTime * <span class="hljs-number">1000</span>
<span class="hljs-built_in">print</span>(<span class="hljs-string">"运行时间:"</span>, runTime, <span class="hljs-string">"秒"</span>)
- 直接执行 test.py 脚本,运行结果如下:
- 在 C# 中调用 Conut() 函数
// 运行时间测试
Console.WriteLine("C# 开始计时");
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
test.Count();
stopWatch.Stop();
Console.WriteLine($"C# 计时结束{stopWatch.ElapsedMilliseconds}");
执行结果如下:
可以看到,使用 pythonnet 调用 python 脚本会有一定的性能损失,不过在对性能要求不是十分高的条件下是可以接受的。
- 执行 test.pyd 文件,运行结果如下:
从结果可以看出调用 pyd 比原生的 py 文件执行还要快,所以可以使用 pythonnet 来执行 pyd 文件,即实现代码保护又提升了执行效率。