C#/.net程式呼叫python

步、步、為營發表於2023-03-01

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));
    
        dynamic sin = np.sin;
        Console.WriteLine(sin(5));
    
        double c = (double)(np.cos(5) + sin(5));
        Console.WriteLine(c);
    
        dynamic a = np.array(new List<float> { 1, 2, 3 });
        Console.WriteLine(a.dtype);
    
        dynamic b = np.array(new List<float> { 6, 5, 4 }, 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;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}
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()

    sum = 0
    for i in range(10000):
        for j in range(10000):
            sum = sum + i + j
    print("sum = ", sum)

    end = time.perf_counter()
    runTime = end - start
    runTime_ms = runTime * 1000

    print("執行時間:", runTime, "秒")
  • 直接執行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檔案,即實現程式碼保護又提升了執行效率。

相關文章