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

  1. Nuget 安装pythonnet

  2. 设置Runtime.PythonDLL属性,即 pythonxx.dll 路径,xx 为版本号

  3. 设置PythonEngine.PythonHome,即 python.exe 所在路径

  4. 设置PythonEngine.PythonPath,python 脚本所在目录,可以放置多个路径,以分号隔开,但是 pathToVirtualEnv\Lib\site-packages 和 pathToVirtualEnv\Lib 应放在最后

  5. 调用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 来构建专用的虚拟环境。

  1. 创建专用虚拟环境(windows 下首先切换到要建立虚拟环境的根目录下),执行conda create --prefix=F:\condaenv\env_name python=3.7 路径及 python 版本根据需要自定义。

  2. 使用 Anaconda Prompt,激活虚拟环境conda activate F:\condaenv\env_name

  3. 本次测试第三方库 Numpy(如果需要其他库,安装方法相同),安装 Numpypip 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&lt;<span class="hljs-built_in">float</span>&gt; { <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&lt;<span class="hljs-built_in">float</span>&gt; { <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();
    

    }

    image-20230301123243892

    注意: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

image-20230301140858858

调用 pyd 文件

pyd 文件主要有以下 2 点作用:

  1. 安全性更高:通过 pyd 生成的文件,已变成了 dll 文件,无法查看源码
  2. 编译成 pyd 后,性能会有提升

将.py 文件编译成 pyd 文件步骤如下:

  1. pip install cython
  2. 在.py 文件目录下创建 setup.py 文件
from distutils.core import setup
from Cython.Build import cythonize

setup(
name = "testName",
ext_modules = cythonize("test.py"), #将 test.py 文件编译成 pyd
)

  1. 执行编译命令

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 脚本,运行结果如下:

image-20230301144439558

  • 在 C# 中调用 Conut() 函数
// 运行时间测试
Console.WriteLine("C# 开始计时");
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
test.Count();
stopWatch.Stop();
Console.WriteLine($"C# 计时结束{stopWatch.ElapsedMilliseconds}");

执行结果如下:

image-20230301144923477

可以看到,使用 pythonnet 调用 python 脚本会有一定的性能损失,不过在对性能要求不是十分高的条件下是可以接受的。

  • 执行 test.pyd 文件,运行结果如下:

image-20230301145141422

从结果可以看出调用 pyd 比原生的 py 文件执行还要快,所以可以使用 pythonnet 来执行 pyd 文件,即实现代码保护又提升了执行效率。