首页 > 编程语言 >C# 与 Python 代码互相调用的实践

C# 与 Python 代码互相调用的实践

时间:2024-11-28 19:58:52浏览次数:6  
标签:调用 C# 代码 Python result COM

一、引言

在当今的软件开发领域,不同的编程语言都有其独特的优势和适用场景。C# 是一种功能强大、面向对象的编程语言,主要应用于 Windows 平台开发、企业级应用开发以及游戏开发(借助 Unity 引擎等)等领域;而 Python 则以其简洁的语法、丰富的库以及在数据科学、机器学习、自动化脚本等众多方面的出色表现备受青睐。在实际的项目开发中,有时候我们希望能够结合这两种语言的优势,实现 C# 与 Python 代码的互相调用,这不仅可以充分利用已有的代码资源,还能拓展项目的功能边界。

二、C# 调用 Python 代码的方式

(一)使用 IronPython

  1. IronPython 简介
    IronPython 是 Python 语言在.NET 平台上的实现,它允许 Python 代码与.NET 框架无缝集成,也就意味着可以很方便地在 C# 程序中调用 Python 代码。IronPython 具备 Python 语言本身的简洁灵活的特性,同时又能利用.NET 平台的诸多资源,比如访问.NET 类库等。
  2. 环境搭建
    首先需要安装 IronPython,通常可以通过 NuGet 包管理器在 Visual Studio 项目中添加对 IronPython 的引用。在 Visual Studio 中,右键点击项目,选择 “管理 NuGet 包”,然后在搜索框中输入 “IronPython” 并安装相应的包。安装完成后,项目中就可以引入相关的命名空间来开始使用 IronPython 了。
  3. 基本调用示例
    以下是一个简单的示例,展示如何在 C# 中使用 IronPython 执行一段简单的 Python 代码:
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;

class Program
{
    static void Main()
    {
        // 创建 Python 运行时环境
        ScriptEngine pythonEngine = Python.CreateEngine();
        // 创建一个脚本作用域
        ScriptScope scope = pythonEngine.CreateScope();

        // 执行 Python 代码
        string pythonCode = "print('Hello from Python in C#!')";
        pythonEngine.Execute(pythonCode, scope);

        // 可以向 Python 代码传递变量并获取结果
        scope.SetVariable("num", 5);
        string pythonCodeWithVariable = "result = num * 2; print('Result in Python:', result)";
        pythonEngine.Execute(pythonCodeWithVariable, scope);

        dynamic result = scope.GetVariable("result");
        Console.WriteLine("Result in C#: " + result);
    }
}

在这个示例中:

  • 首先通过 Python.CreateEngine() 创建了 Python 的运行时环境。
  • 接着使用 CreateScope() 建立了脚本作用域,它类似于 Python 代码执行时的全局和局部变量空间。
  • 然后可以直接将 Python 代码字符串传递给 Execute 方法来执行,如打印简单的问候语句。并且还能向 Python 代码所在的作用域设置变量(如设置 num 变量),Python 代码对变量进行操作后,又可以从作用域中获取结果返回给 C# 代码(获取 result 变量)。
  1. 调用 Python 函数和模块
    当要调用 Python 中定义的函数和模块时,过程会稍有不同。假设在 Python 中有如下模块 math_functions.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

在 C# 中调用这些函数的代码如下:

using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
using System.IO;

class Program
{
    static void Main()
    {
        ScriptEngine pythonEngine = Python.CreateEngine();
        ScriptScope scope = pythonEngine.CreateScope();

        // 将包含 Python 模块的目录添加到搜索路径
        string modulePath = Path.Combine(Directory.GetCurrentDirectory(), "");
        pythonEngine.GetSearchPaths().Add(modulePath);

        // 导入 Python 模块
        string moduleName = "math_functions";
        dynamic mathModule = pythonEngine.ImportModule(moduleName);

        // 调用模块中的函数
        int resultAdd = mathModule.add(3, 5);
        Console.WriteLine("Result of add: " + resultAdd);

        int resultMultiply = mathModule.multiply(4, 6);
        Console.WriteLine("Result of multiply: " + resultMultiply);
    }
}

这里:

  • 先获取 Python 引擎的搜索路径并添加包含 Python 模块文件的目录,确保可以找到模块。
  • 通过 ImportModule 方法导入 Python 模块,然后就可以像使用普通对象的方法一样调用模块中定义的函数了,分别获取加法和乘法运算的结果并在 C# 中输出。

(二)通过进程间通信(IPC)方式

  1. 概述
    当不想依赖于 IronPython 这种特定的 Python 在.NET 上的实现时,可以采用进程间通信的方式来让 C# 调用 Python 代码。常见的 IPC 机制有标准输入输出流、命名管道、Socket 等。这种方式的基本思路是在 C# 程序中启动一个 Python 进程,然后通过特定的通信方式向 Python 进程传递参数、发送执行指令等,再获取 Python 进程返回的结果。
  2. 使用标准输入输出流(stdin/stdout)
  • Python 端代码准备:例如,创建一个简单的 Python 脚本 sum_numbers.py 用于计算输入数字的和,代码如下:
import sys

nums = [int(arg) for arg in sys.stdin.readline().split()]
result = sum(nums)
print(result)
  • C# 调用代码示例
using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        string pythonExePath = @"C:\Python\Python38\python.exe";  // Python 解释器的路径,根据实际情况修改
        string pythonScriptPath = Path.Combine(Directory.GetCurrentDirectory(), "sum_numbers.py");

        ProcessStartInfo startInfo = new ProcessStartInfo(pythonExePath, pythonScriptPath);
        startInfo.RedirectStandardInput = true;
        startInfo.RedirectStandardOutput = true;
        startInfo.UseShellExecute = false;
        startInfo.CreateNoWindow = true;

        Process process = new Process();
        process.StartInfo = startInfo;
        process.Start();

        // 向 Python 脚本传递参数
        using (StreamWriter writer = process.StandardInput)
        {
            writer.WriteLine("1 2 3 4 5");
        }

        // 获取 Python 脚本返回的结果
        string result = process.StandardOutput.ReadToEnd();
        Console.WriteLine("Result from Python: " + result);

        process.WaitForExit();
    }
}

在这个示例中:

  • 首先指定了 Python 解释器的路径以及要执行的 Python 脚本的路径,创建了 ProcessStartInfo 对象来配置启动进程的相关参数,比如重定向标准输入输出流等。
  • 启动 Python 进程后,通过 StreamWriter 向 Python 脚本的标准输入写入参数(这里是一串数字),Python 脚本读取这些参数进行计算并将结果输出到标准输出。
  • 最后在 C# 中通过读取标准输出流获取 Python 脚本返回的结果。
  1. 使用命名管道(Named Pipes)
  • 命名管道原理及优势:命名管道提供了一种可靠的、基于文件系统的进程间通信机制,多个进程可以通过命名管道进行双向通信,它在安全性和数据传输的有序性等方面有较好的表现。
  • Python 端实现示例(部分代码)
import os
import sys
import win32pipe
import win32file

PIPE_NAME = r'\\.\pipe\my_python_csharp_pipe'

def pipe_server():
    pipe = win32pipe.CreateNamedPipe(
        PIPE_NAME,
        win32pipe.PIPE_ACCESS_DUPLEX,
        win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
        1, 65536, 65536, 0, None
    )
    win32pipe.ConnectNamedPipe(pipe, None)
    data = win32file.ReadFile(pipe, 65536)
    # 对接收的数据进行处理,例如解析参数等
    processed_data = process_data(data[1].decode('utf-8'))
    win32file.WriteFile(pipe, processed_data.encode('utf-8'))
    win32pipe.DisconnectNamedPipe(pipe)
    win32file.CloseHandle(pipe)
  • C# 端实现示例(部分代码)
using System.IO.Pipes;

class Program
{
    static void Main()
    {
        string pipeName = @"\\.\pipe\my_python_csharp_pipe";
        using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut))
        {
            pipeClient.Connect();
            using (StreamWriter writer = new StreamWriter(pipeClient))
            {
                writer.Write("Some data to send to Python");
                writer.Flush();
            }
            using (StreamReader reader = new StreamReader(pipeClient))
            {
                string result = reader.ReadToEnd();
                Console.WriteLine("Result from Python via named pipe: " + result);
            }
        }
    }
}

在这个命名管道的示例中:

  • Python 端创建了命名管道服务器,等待 C# 客户端连接,接收到 C# 发送的数据后进行处理,再将处理结果返回给 C# 端。
  • C# 端则作为客户端连接到命名管道,发送数据给 Python 并读取 Python 返回的结果,通过这样的双向通信实现了代码间的交互调用。
  1. 使用 Socket 通信
  • Socket 通信基础:Socket 通信基于网络协议,可以实现不同主机或者同一主机上不同进程之间的通信,通过指定 IP 地址和端口号来建立连接。
  • Python 端示例代码(简单的 TCP 服务器示例)
import socket

HOST = '127.0.0.1'
PORT = 8888

def socket_server():
    with socket.socket(socket.AF_INET, socket.PTYPE_TCP) as s:
        s.bind((HOST, PORT))
        s.listen()
        conn, addr = s.accept()
        with conn:
            data = conn.recv(1024)
            processed_data = process_data(data.decode('utf-8'))
            conn.sendall(processed_data.encode('utf-8'))
  • C# 端示例代码(TCP 客户端示例)
using System.Net.Sockets;
using System.Text;

class Program
{
    static void Main()
    {
        string host = "127.0.0.1";
        int port = 8888;
        using (TcpClient client = new TcpClient(host, port))
        {
            using (NetworkStream stream = client.GetStream())
            {
                byte[] dataToSend = Encoding.UTF8.GetBytes("Some data for Python");
                stream.Write(dataToSend, 0, dataToSend.Length);
                byte[] buffer = new byte[1024];
                int bytesRead = stream.Read(buffer, 0, buffer.Length);
                string result = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Result from Python via socket: " + result);
            }
        }
    }
}

这里通过建立 TCP 连接,C# 客户端向 Python 服务器端发送数据,Python 服务器处理后将结果返回给 C# 客户端,实现了跨语言的通信和代码调用。

(三)借助第三方库(如 Pythonnet)

  1. Pythonnet 简介
    Pythonnet 是一个强大的开源项目,它为.NET 平台提供了与 Python 进行交互的功能。它允许在.NET 应用程序(包括 C# 程序)中直接导入并使用 Python 模块、调用 Python 函数等,就好像 Python 代码是.NET 代码的一部分一样,底层通过与 Python 解释器进行交互来实现这些功能。
  2. 环境搭建与配置
    要使用 Pythonnet,首先需要通过 NuGet 包管理器在 C# 项目中安装 Python.Runtime 包。安装完成后,在 C# 代码中引入 Python.Runtime 命名空间就可以开始使用相关功能了。另外,需要确保系统中已经安装了相应版本的 Python 解释器,并且配置好环境变量,以便 Pythonnet 可以找到并调用 Python 相关资源。
  3. 基本调用示例
    以下是一个简单示例展示如何使用 Pythonnet 在 C# 中调用 Python 函数:
using Python.Runtime;

class Program
{
    static void Main()
    {
        using (Py.GIL())  // 获取全局解释器锁,用于多线程环境下安全访问 Python 解释器
        {
            dynamic mathModule = Py.Import("math");
            double result = mathModule.sqrt(16);
            Console.WriteLine("Result of square root: " + result);
        }
    }
}

在这个示例中:

  • 首先通过 Py.GIL() 获取全局解释器锁,这是在多线程环境下确保安全访问 Python 解释器的必要操作。
  • 然后使用 Py.Import 方法导入 Python 的 math 模块,就可以像使用.NET 中的对象一样调用模块中的函数(这里调用了 sqrt 函数计算平方根),并将结果输出到控制台。
  1. 传递复杂数据结构和处理回调函数
    Pythonnet 还支持在 C# 和 Python 之间传递复杂的数据结构,比如列表、字典等。同时,也可以处理 Python 函数作为回调函数传递到 C# 中使用等情况。例如:
using Python.Runtime;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        using (Py.GIL())
        {
            // 传递列表数据结构到 Python 函数
            List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
            PyObject pyList = ToPython(numbers);
            dynamic pythonFunction = Py.Import("my_module").my_function;
            PyObject result = pythonFunction(pyList);
            List<int> processedNumbers = FromPython<List<int>>(result);

            // 处理回调函数示例(假设 Python 中有一个函数接受回调函数作为参数)
            dynamic callbackFunction = (Func<double, double>)((x) => x * 2);
            PyObject pyCallback = ToPython(callbackFunction);
            dynamic anotherPythonFunction = Py.Import("another_module").function_with_callback;
            PyObject finalResult = anotherPythonFunction(pyCallback);
            Console.WriteLine("Final result: " + FromPython<double>(finalResult));
        }
    }

    static PyObject ToPython<T>(T obj)
    {
        return Converter.ToPython(obj);
    }

    static T FromPython<T>(PyObject pyObj)
    {
        return Converter.ToManaged<T>(pyObj);
    }
}

在上述代码中:

  • 展示了如何将 C# 中的列表数据结构转换为 Python 中的列表(通过 ToPython 方法等)传递给 Python 函数进行处理,再将处理后的结果转换回 C# 中的列表。
  • 同时也演示了如何将 C# 中的委托(这里是一个简单的计算函数)转换为可以在 Python 中使用的回调函数形式,传递给 Python 函数,实现更复杂的交互逻辑。

三、Python 调用 C# 代码的方式

(一)使用 Ctypes 库

  1. Ctypes 简介
    Ctypes 是 Python 内置的一个用于调用动态链接库(如 Windows 下的 DLL 文件,Linux 下的 SO 文件等)的标准库。通过 Ctypes,Python 可以加载 C# 编译生成的动态链接库,并调用其中定义的函数,实现与 C# 代码的交互。不过需要注意的是,C# 代码需要按照一定的规则编译成可供 Ctypes 使用的动态链接库格式。
  2. C# 代码编译为动态链接库
    首先,在 Visual Studio 中创建一个 C# 类库项目。例如,创建一个简单的类库项目,包含以下代码:
using System;
using System.Runtime.InteropServices;

namespace MyCSharpLibrary
{
    public class MathFunctions
    {
        [DllExport("add_numbers", CallingConvention = CallingConvention.Cdecl)]
        public static int AddNumbers(int a, int b)
        {
            return a + b;
        }
    }
}

这里定义了一个简单的函数 AddNumbers 用于加法运算,并且通过 [DllExport] 属性(需要引用相关的库来支持这个属性,比如 UnmanagedExports 库)来标记这个函数可以被导出供外部调用,同时指定了调用约定为 Cdecl。然后在 Visual Studio 中,将项目的输出类型设置为 “类库(DLL)” 进行编译,生成对应的 DLL 文件。
3. Python 调用示例
在 Python 中调用上述生成的 C# DLL 文件中的函数代码如下

from ctypes import CDLL

# 加载C#编译生成的DLL文件,这里的路径根据实际生成的DLL位置进行修改
my_dll = CDLL('path/to/MyCSharpLibrary.dll')

# 指定函数的参数类型和返回值类型,要和C#中定义的保持一致
my_dll.add_numbers.argtypes = [ctypes.c_int, ctypes.c_int]
my_dll.add_numbers.restype = ctypes.c_int

# 调用C#中定义的函数
result = my_dll.add_numbers(3, 5)
print("调用C#函数得到的结果:", result)

在这个示例中:

  • 首先通过 CDLL 函数加载了 C# 编译生成的 DLL 文件,需要准确指定其路径,确保 Python 能够找到该文件。
  • 接着使用 argtypes 属性来定义 add_numbers 函数的参数类型,以及使用 restype 属性定义返回值类型,这一步非常关键,它使得 Python 能够正确地按照 C# 中函数定义的要求来传递参数和处理返回值。
  • 最后就可以像调用普通 Python 函数一样调用 C# 中导出的 add_numbers 函数,并获取返回的结果进行输出。

(二)借助 Python.NET

  1. Python.NET 原理及优势
    Python.NET 不仅支持在 C# 中调用 Python 代码,反过来在 Python 中也可以调用 C# 代码。它在 Python 和.NET 之间搭建了一座桥梁,使得两种语言的代码能够深度交互融合。利用 Python.NET,Python 可以直接访问.NET 程序集(比如 C# 编译生成的 DLL 文件等),并调用其中的类、方法等,充分利用 C# 编写的功能强大的组件和逻辑。

  2. 环境配置与准备
    要使用 Python.NET 在 Python 中调用 C# 代码,首先需要在 Python 环境中安装 pythonnet 包。可以通过 pip install pythonnet 命令来进行安装(前提是 Python 环境已经正确配置且网络连接正常)。同时,确保相应的 C# 项目已经编译生成了可供调用的程序集(通常是 DLL 文件),并且知晓其所在的路径。

  3. 基本调用示例
    假设 C# 中有如下一个简单的类库项目(编译成了 DLL 文件),定义了一个简单的 Greeting 类:

using System;

namespace MyCSharpLibrary
{
    public class Greeting
    {
        public string SayHello()
        {
            return "Hello from C#!";
        }
    }
}

在 Python 中调用这个 C# 类中的方法示例如下:

import clr  # pythonnet 中的关键模块,用于加载.NET 程序集

# 加载C#编译生成的程序集,根据实际的DLL文件路径修改
clr.AddReference('path/to/MyCSharpLibrary.dll')

# 导入对应的命名空间,这里就是C#中的MyCSharpLibrary命名空间
from MyCSharpLibrary import Greeting

# 创建C#类的实例并调用其中的方法
greeting_obj = Greeting()
result = greeting_obj.SayHello()
print(result)

在这个示例中:

  • 首先通过 clr.AddReference 语句加载了 C# 编译生成的程序集(DLL 文件),这一步告诉 Python 要使用哪些.NET 相关的组件。
  • 然后从对应的命名空间(C# 中的 MyCSharpLibrary 命名空间)中导入了 Greeting 类,这使得 Python 能够识别并操作这个类。
  • 最后创建了 Greeting 类的实例,并调用其 SayHello 方法获取返回的字符串结果,再在 Python 中将结果进行输出。
  1. 调用复杂功能及传递数据示例
    更复杂一些的情况,比如 C# 中有涉及复杂数据类型和更多方法交互的类,以下是一个示例。C# 代码如下:
using System.Collections.Generic;

namespace MyCSharpLibrary
{
    public class DataManipulation
    {
        public List<int> DoubleNumbers(List<int> inputList)
        {
            List<int> resultList = new List<int>();
            foreach (int num in inputList)
            {
                resultList.Add(num * 2);
            }
            return resultList;
        }
    }
}

Python 中调用这个类中方法并且传递数据的代码如下:

import clr
clr.AddReference('path/to/MyCSharpLibrary.dll')
from MyCSharpLibrary import DataManipulation

# 创建C#类的实例
data_manipulation_obj = DataManipulation()

# 在Python中准备要传递给C#的数据,这里是一个整数列表
input_data = [1, 2, 3, 4, 5]

# 将Python列表转换为.NET中的List<int>类型,pythonnet提供了相应转换机制
from System.Collections.Generic import List
dotnet_list = List[int](input_data)

# 调用C#类中的方法并获取结果
result_list = data_manipulation_obj.DoubleNumbers(dotnet_list)

# 将结果转换回Python列表并输出
python_result = list(result_list)
print(python_result)

在上述代码中:

  • 先是加载了包含 DataManipulation 类的 C# 程序集,并创建了该类的实例。
  • 接着在 Python 中准备了一个简单的整数列表作为输入数据,但是由于 C# 中方法接收的是 List<int> 类型(.NET 中的类型),所以需要借助 pythonnet 提供的机制将 Python 列表转换为对应的.NET 类型。
  • 调用 C# 类中的 DoubleNumbers 方法后,得到的返回结果也是.NET 中的 List<int> 类型,再将其转换回 Python 列表并输出展示,体现了在不同语言间传递和处理复杂数据类型的过程。

(三)通过 COM 组件(适用于 Windows 环境)

  1. COM 组件概述及适用性
    COM(Component Object Model,组件对象模型)是微软提出的一种软件组件交互的规范,在 Windows 环境下广泛应用。如果将 C# 代码封装成 COM 组件,Python 就可以通过 COM 相关的机制来调用 C# 代码。这种方式特别适合在 Windows 平台上,当需要在不同语言编写的应用程序之间实现互操作性,且希望利用 COM 成熟的架构和功能时使用。

  2. 将 C# 代码封装为 COM 组件
    在 Visual Studio 中创建一个 C# 类库项目,然后按照以下步骤将其转换为 COM 组件:

  • 首先,需要将项目的 “程序集信息” 属性中的 “使程序集 COM 可见” 选项勾选上,这样才能确保生成的组件对外可通过 COM 机制访问。
  • 接着,对于要暴露给外部(也就是通过 COM 可调用)的类和接口等,需要添加合适的 COM 相关的特性。例如:
using System;
using System.Runtime.InteropServices;

namespace MyCSharpCOMComponent
{
    [Guid("12345678-1234-1234-1234-123456789ABC")]  // 全局唯一标识符,需要保证唯一性
    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IMyInterface
    {
        string GetMessage();
    }

    [Guid("23456789-2345-2345-2345-23456789ABC1")]
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class MyCOMClass : IMyInterface
    {
        public string GetMessage()
        {
            return "Message from COM-enabled C# class";
        }
    }
}

在上述代码中:

  • 通过 [Guid] 特性为接口和类定义了全局唯一标识符(GUID),这是 COM 组件识别和区分不同组件、接口等的重要标识。
  • [ComVisible(true)] 表示这些类型是可以通过 COM 机制对外可见的。
  • 定义了接口 IMyInterface 以及实现该接口的类 MyCOMClass,类中实现了 GetMessage 方法,这个方法就是后续通过 COM 可以被 Python 调用的内容。

编译该项目后,会生成相应的 DLL 文件,还需要使用注册工具(如 regasm.exe)对生成的组件进行注册,命令示例如下(以管理员身份运行命令提示符):

regasm.exe path/to/MyCSharpCOMComponent.dll /tlb:MyCSharpCOMComponent.tlb /codebase

这个命令会将组件注册到系统中,并生成对应的类型库(TLB 文件),方便后续 Python 等语言通过 COM 进行查找和调用。

  1. Python 调用 COM 组件示例
    在 Python 中,可以使用 win32com 库(需要先安装 pywin32 包)来调用已注册的 COM 组件,代码示例如下:
import win32com.client

# 创建COM组件对象实例
com_obj = win32com.client.Dispatch("MyCSharpCOMComponent.MyCOMClass")

# 调用COM组件中的方法
result = com_obj.GetMessage()
print(result)

在这个示例中:

  • 首先通过 win32com.client.Dispatch 函数,根据 COM 组件的名称(这里是 MyCSharpCOMComponent.MyCOMClass,对应 C# 中定义的组件名称和类名)创建了组件对象的实例。
  • 然后就可以像调用普通 Python 对象的方法一样调用 COM 组件中定义的 GetMessage 方法,并获取返回的结果进行输出。

(四)利用 RPC(远程过程调用,可选跨平台场景)

  1. RPC 基本原理及优势
    RPC(Remote Procedure Call)是一种允许不同进程(可以在同一台机器上,也可以在不同机器上,跨网络环境)之间进行函数调用的机制,就好像调用本地函数一样方便。在 Python 调用 C# 代码的场景中,如果涉及到分布式系统或者希望实现更灵活的跨平台通信与代码调用,RPC 是一种可行的选择。常见的 RPC 框架有 gRPC、Thrift 等,它们都有各自的特点和适用场景,这里以 gRPC 为例进行介绍。

  2. 使用 gRPC 实现 Python 调用 C# 代码

(1)定义服务接口(使用 Protocol Buffers)

首先,需要使用 Protocol Buffers(一种与语言无关的序列化数据结构的方式)来定义服务接口。创建一个 .proto 文件,例如 my_service.proto,内容示例如下:

syntax = "google.protobuf3";

package my_service;

// 定义请求消息结构
message RequestMessage {
    int32 num = 1;
}

// 定义响应消息结构
message ResponseMessage {
    int32 result = 1;
}

// 定义服务接口
service MyService {
    rpc DoubleNumber(RequestMessage) returns (ResponseMessage) {}
}

在这个文件中:

  • 定义了请求消息和响应消息的结构,分别包含了相应的数据字段(这里简单地以整数为例)。
  • 定义了 MyService 服务接口,其中包含了一个 DoubleNumber 方法,它接收 RequestMessage 类型的请求,并返回 ResponseMessage 类型的响应,意味着这个方法将用于实现把输入的数字翻倍的功能(只是示例功能)。
(2)C# 端实现服务
  • 使用 gRPC 相关的 C# 库(通常通过 NuGet 包管理器安装),按照定义的 .proto 文件来生成 C# 代码对应的接口和类等基础结构。
  • 然后创建一个具体的服务类来实现 MyService 接口,示例代码如下:
using Grpc.Core;
using my_service;

namespace MyCSharpGRPCService
{
    public class MyServiceImpl : MyService.MyServiceBase
    {
        public override Task<ResponseMessage> DoubleNumber(RequestMessage request, ServerCallContext context)
        {
            int result = request.num * 2;
            ResponseMessage response = new ResponseMessage { result = result };
            return Task.FromResult(response);
        }
    }
}

在这个 C# 服务类中:

  • 继承自 MyService.MyServiceBase(由 gRPC 根据 .proto 文件生成的基类),并实现了 DoubleNumber 方法,在方法中获取请求消息中的数字并将其翻倍,构造响应消息返回。

  • 接着需要启动 gRPC 服务,示例代码如下:

using System;
using System.Threading.Tasks;
using Grpc.Core;
using my_service;

namespace MyCSharpGRPCService
{
    class Program
    {
        const int Port = 50051;
        static async Task Main()
        {
            Server server = new Server
            {
                Services = { MyService.BindService(new MyServiceImpl()) },
                Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
            };
            server.Start();
            Console.WriteLine($"gRPC server started on port {Port}");
            await server.ShutdownTask;
        }
    }
}

这里创建了一个 gRPC 服务器实例,绑定了 MyServiceImpl 服务,并指定了监听的端口(这里是 50051),启动服务器后就可以等待客户端(Python 端)的调用了。

(3)Python 端调用服务
  • 同样,在 Python 端也需要使用 gRPC 相关的 Python 库(通常通过 pip 安装),根据 .proto 文件生成对应的 Python 代码结构。
  • 然后编写调用代码,示例如下:
import grpc
from my_service import my_service_pb2, my_service_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = my_service_pb2_grpc.MyServiceStub(channel)
        request = my_service_pb2.RequestMessage(num=5)
        response = stub.DoubleNumber(request)
        print("Result from gRPC call:", response.result)

if __name__ == '__main__':
    run()

在 Python 调用代码中:

  • 首先通过 grpc.insecure_channel 建立了与 C# 端 gRPC 服务器的连接(指定了服务器的地址和端口)。
  • 接着创建了服务的存根(stub),它相当于客户端与服务器通信的代理对象。
  • 构造了请求消息(这里设置请求数字为 5),并通过存根调用 DoubleNumber 服务方法,获取响应消息并输出其中的结果,实现了 Python 对 C# 中通过 gRPC 提供的服务功能的调用。

四、性能比较与适用场景分析

(一)性能比较

  1. 使用 IronPython 方式
    当使用 IronPython 在 C# 中调用 Python 代码时,由于 IronPython 本身是基于.NET 平台对 Python 的实现,在一定程度上与.NET 框架集成较好,性能相对比较稳定。不过,相比于直接运行原生的 Python 代码,可能会因为中间的转换和适配层等因素存在一定的性能损耗。尤其是在执行一些计算密集型或者对性能要求极高的任务时,这种损耗可能会更加明显。例如,在一个循环中频繁调用 Python 函数进行复杂数学运算,相比直接用 C# 实现相同功能,整体执行时间可能会有所增加。

  2. 进程间通信(IPC)方式
    采用进程间通信方式,如标准输入输出流、命名管道、Socket 等,性能往往受到通信开销的影响。每次传递数据都需要经过序列化、传输、反序列化等过程,对于大量数据或者频繁调用的情况,这个开销会累积起来,导致整体性能下降。例如,通过 Socket 通信在网络环境中进行 C# 和 Python 代码交互,如果网络状况不佳或者传输的数据量较大,会明显感觉到延迟增加,执行效率变低。

  3. 借助第三方库(如 Pythonnet)方式
    Pythonnet 在性能方面表现相对较好,它能比较高效地在 C# 和 Python 之间进行交互,因为其底层做了一定的优化来减少数据转换和调用的开销。但同样,与纯粹在一种语言环境内执行代码相比,还是会有一些额外的成本,特别是在处理复杂的数据结构传递或者频繁的跨语言函数调用场景下。

  4. Python 调用 C# 相关方式性能特点

    • Ctypes 库方式:通过 Ctypes 调用 C# 编译成的动态链接库,性能主要取决于 C# 代码本身的效率以及数据在两种语言间转换的开销。由于需要严格匹配参数类型和处理返回值类型等,不当的使用可能会引入一些性能瓶颈,不过对于简单的函数调用场景,如果 C# 代码执行的操作效率较高,整体性能可以满足很多常规需求。
    • Python.NET 方式:与前面在 C# 调用 Python 中提到的 Python.NET 类似,它在性能上相对平衡,在处理不同语言间的数据交互和函数调用上有一定的效率保障,但同样无法避免跨语言交互带来的一些额外开销,尤其是在大规模、高性能要求的应用场景下需要综合考虑优化措施。
    • COM 组件方式(Windows 环境)
    • 基于 COM 组件的调用方式,性能受到 COM 机制本身的影响,其内部有一套自己的对象管理、接口调用等流程,在启动和初始化阶段可能会有一定的时间消耗,不过对于一些基于 Windows 平台且对性能要求不是极致苛刻的应用场景来说,通常能够满足需求。例如在简单的 Windows 桌面应用中,Python 通过 COM 调用 C# 编写的组件来实现特定功能,其性能损耗在可接受范围内。但如果是频繁地创建和销毁 COM 对象,或者进行大量数据交互的复杂操作时,可能会因为 COM 本身的复杂性以及额外的内存管理等因素,导致整体性能有所下降。

    • RPC(远程过程调用,可选跨平台场景)方式:像 gRPC 这类基于 RPC 的调用方式,性能开销主要体现在网络传输、序列化和反序列化数据等方面。在网络状况良好且数据量较小的情况下,其性能表现尚可,能够实现较为流畅的跨语言代码调用,适用于分布式系统或者不同机器间的函数调用场景。然而,一旦涉及到大数据量传输或者高并发的频繁调用,网络带宽的限制以及序列化反序列化的时间成本就会凸显出来,导致整体的响应时间变长,执行效率降低。例如在一个需要实时处理大量数据且频繁在 Python 和 C# 之间进行远程函数调用的物联网应用场景中,如果没有对 RPC 进行优化配置,性能可能无法满足快速响应的要求。

  • (二)适用场景分析

  • 使用 IronPython 方式适用场景
    IronPython 非常适合在.NET 项目中需要临时嵌入一些 Python 脚本逻辑的情况。比如在一个基于 Windows Forms 或者 WPF 开发的桌面应用程序中,开发人员想用 Python 的简洁语法快速实现一些特定的业务规则验证、数据处理逻辑等,而又不想脱离.NET 框架的环境,这时利用 IronPython 可以很方便地在 C# 代码中直接调用 Python 代码,实现快速开发和集成。并且对于熟悉 Python 语言的开发者来说,能够在 C# 主导的项目里复用自己的 Python 知识和代码片段,降低开发门槛。

  • 进程间通信(IPC)方式适用场景

    • 标准输入输出流(stdin/stdout):适用于简单的一次性任务交互,比如 C# 程序需要调用 Python 脚本完成一个特定的文件解析任务,将文件路径等参数传递给 Python 脚本,Python 脚本解析完后返回结果给 C# 程序。这种方式实现起来相对简单,对于少量数据和不频繁的调用场景比较方便,常用于一些小型的自动化脚本组合项目中。
    • 命名管道(Named Pipes):更适合在同一台机器上不同进程间需要安全、可靠且高效的双向通信场景。例如,在一个本地的企业级应用中,C# 编写的后台服务进程和 Python 编写的数据分析进程需要频繁交互数据,传递诸如配置信息、中间计算结果等,命名管道可以提供有序的数据传输和较好的安全性保障,确保通信的稳定性。
    • Socket 通信:当涉及到跨网络环境,比如不同服务器上的 C# 应用和 Python 应用需要相互协作,或者开发分布式系统中不同节点间(可能基于不同语言实现)需要进行通信和功能调用时,Socket 通信就发挥了作用。比如在一个云计算平台中,C# 编写的前端管理服务和 Python 编写的后端数据处理服务分布在不同的服务器上,通过 Socket 通信实现任务的调度和数据的交互,以共同完成复杂的业务流程。
  • 借助第三方库(如 Pythonnet)方式适用场景
    Pythonnet 适用于需要深度融合 C# 和 Python 两种语言优势的项目。例如在一个机器学习项目中,核心的算法模型训练和数据分析部分可能是用 Python 编写的,因为有丰富的相关库(如 TensorFlow、PyTorch 等),而项目的前端界面展示或者与底层系统交互的部分用 C# 开发(借助其强大的 Windows 平台开发能力和图形化界面开发能力等),通过 Pythonnet 可以方便地在这两部分代码之间进行数据传递、函数调用,实现完整的项目功能,让不同语言编写的模块紧密协作,发挥各自最大的优势。

  • Python 调用 C# 相关方式适用场景

    • Ctypes 库方式:适合于对已有的 C# 代码库进行简单复用的场景,尤其是当 C# 代码实现了一些特定的基础功能,如高效的数值计算、底层系统调用等,而 Python 项目需要使用这些功能时,可以将 C# 代码编译成动态链接库,然后通过 Ctypes 在 Python 中调用。比如在一个 Python 编写的科学计算软件中,需要调用 C# 实现的高精度数值算法模块,利用 Ctypes 就能快速集成进来,扩充软件的功能。
    • Python.NET 方式:类似于在 C# 调用 Python 中的应用场景,在需要紧密结合 C# 和 Python 代码,实现复杂业务逻辑交互的项目中很有用。例如在开发一款游戏辅助工具时,游戏本身的部分逻辑是用 C# 编写(可能基于 Unity 引擎等),而工具中的数据分析、自动化操作等功能想用 Python 实现,通过 Python.NET 可以方便地让 Python 代码调用游戏相关的 C# 代码,实现各种定制化功能,提升游戏体验。
    • COM 组件方式(Windows 环境):主要适用于在 Windows 平台上基于 COM 生态系统的应用开发场景。比如在 Office 插件开发中,Python 可以通过 COM 调用 C# 编写的组件来扩展 Office 应用(如 Word、Excel 等)的功能,实现特定的文档处理、数据分析等操作,充分利用 COM 在 Windows 应用间互操作性方面的优势,实现跨语言的功能扩展。
    • RPC(远程过程调用,可选跨平台场景)方式:适用于分布式系统、微服务架构等跨平台跨机器的应用场景。例如在一个电商系统中,不同的业务模块可能分别用 C# 和 Python 开发,并且部署在不同的服务器上,通过 RPC(如 gRPC)可以方便地实现各个模块之间的远程函数调用,如库存查询(可能用 C# 实现)与订单处理(可能用 Python 实现)之间的交互,实现系统整体的协同运作,而不受限于语言和服务器的差异。

五、常见问题及解决方法

(一)版本兼容性问题

  • Python 与相关库版本兼容性
    不同版本的 Python 对一些用于跨语言调用的库(如 pythonnet、ctypes 等)的支持情况可能有所不同。例如,较老版本的 Python 可能无法很好地兼容最新版本的 pythonnet,会出现安装失败或者运行时出现莫名错误的情况。解决方法是查看相应库的官方文档,了解其支持的 Python 版本范围,尽量选择匹配的版本进行安装和使用。如果项目中已经确定了 Python 版本,而某个关键库不兼容,可以考虑寻找该库的旧版本进行适配,或者尝试升级 Python 版本(但要确保项目中的其他依赖不会受到影响)。

  • C# 与相关组件、框架版本兼容性
    同样, C# 项目中使用的一些用于跨语言交互的组件(如 IronPython、用于生成 COM 组件的相关工具等)也有版本兼容性问题。比如,IronPython 的某个版本可能与当前使用的.NET 框架版本存在不兼容,导致在创建 Python 运行时环境或者执行 Python 代码时出现错误。这时需要核对 IronPython 版本与.NET 框架版本的兼容性要求,通过升级或降级相应的组件来解决问题。对于 COM 组件相关的情况,不同版本的 Visual Studio 生成的 COM 组件在注册和被 Python 调用时可能也会有差异,要确保按照对应版本的要求进行正确的注册和配置。

  • (二)数据类型转换问题

  • 在 C# 调用 Python 中数据类型转换问题     当使用IronPython或Pythonnet从C#调用Python代码时,数据类型转换可能会遇到挑战。例如,C#有自己的类型,如int、double、string等,而Python有自己的数据类型,如nt、float、str,以及更复杂的数据类型如list、dict、tuple。在将变量从C#传递到Python时,确保数据类型正确转换至关重要。

  • 在Python中调用C#中数据类型转换问题    当Python通过Ctypes调用C#代码时,Python需要准确指定所调用的C#函数的参数类型和返回值类型。例如,如果一个C#函数需要一个int参数,但Python在没有正确类型转换的情况下传递了一个float,这将导致错误或不正确的结果。

  • (三)内部管理和资源泄漏问题

  • C#调用Python时的内部管理问题   当使用IronPython或Pythonnet并经常在C#中创建和销毁Python对象或在循环中运行Python代码时,适当的内存管理变得很重要。如果操作不正确,可能会导致内存泄漏。

  • Python调用C#时的内部管理问题  同样,当Python通过各种方式(如Ctypes或COM组件)调用C#代码时,可能会出现与内存管理相关的问题。例如,当使用Ctypes调用内部分配内存的C#函数(如返回大型数组或复杂数据结构的函数)时,Python需要确保在使用后正确释放内存,以避免内存泄漏。对于COM组件,了解COM对象的生命周期管理,并在不使用COM对象时在Python中正确释放对COM对象的引用,对于保持应用程序中良好的内存健康至关重要。

  • (四)错误处理问题

  • C#调用Python时的错误处理 当C#调用Python代码时,错误可能会在各个阶段发生。例如,如果正在执行的Python代码中存在语法错误(无论是直接作为IronPython中的字符串还是从模块导入时),都可能导致执行失败。在这种情况下,在C#中进行适当的错误处理以捕获这些异常并优雅地处理它们非常重要。

  • Python调用C#时的错误处理  当Python调用C#代码时,错误也可能发生在不同的场景中。例如,如果在使用Ctypes时无法找到或正确加载C#DLL文件,或者C#函数本身存在错误(如除以零或C#代码中未处理的异常),Python需要能够检测和处理这些错误。

六、总结

总之,相互调用C#和Python代码的能力为软件开发开辟了一个充满可能性的世界。通过结合这两种语言的优势,开发人员可以构建更强大、更通用、更高效的应用程序。有多种方法可以实现这种相互的代码调用,每种方法在性能、适用场景和潜在问题方面都有自己的特点。

标签:调用,C#,代码,Python,result,COM
From: https://blog.csdn.net/m0_60315436/article/details/144118119

相关文章

  • 6CCS3AIN  Pacman MDP-solver
    6CCS3AIN Coursework1IntroductionThiscourseworkexerciseasksyoutowritecodetocreateanMDP-solvertoworkinthePacmanenvironmentthatweusedforthepracticalexercises.Readalltheseinstructionsbeforestarting.Thisexercisewillbeasses......
  • Burp抓PC端WX小程序数据包
    Burp抓PC端WX小程序数据包两种方式,一种为设置系统代理Burp直接抓,一种为使用Proxifire转发数据包至Burp再抓。Way1系统安装Burp证书,安装步骤同上。设置系统代理为Burp,默认127.0.0.1:8080随便打开一个WX小程序,Burp直接抓即可。以某车之家小程序举例WX小程序抓......
  • MAT188 principal components
    MAT188:Homework5BackgroundBelowisanillustrationofthesouthwesternportionofthegreatprovinceofBritishColumbia.Citiesarelabelledinblue,andredcirclesindicatethelocationofpublicweathertations.Includedwiththisassignmentiste......
  • CTF学习(21)MISC(九连环)
    1.查看属性(无果)--->使用010editor打开后发现存在zip文件2.使用foremost分离文件后获得名为00000038.zip的zip文件--->爆破四位无果后查看是否存在伪加密(第一次知道伪加密还能对zip文件内的zip文件进行修改...)3.解压后获得good-已合并.jpg文件和qwe.zip文件--->在kali中使......
  • 利用树莓派Pico制作迷你小台灯:C++与硬件设计结合的分享
    小台灯是一个经典的电子设计项目,结合了LED和按键的基本应用,适合初学者学习控制逻辑和硬件交互。通过树莓派Pico开发板,本项目实现了用按键控制LED灯的开关功能。以下将详细解析项目内容,并结合C++代码讲解背后的核心技术。一、项目概述1.项目目标学习按键与LED的基本原理......
  • LeetCode - #146 LRU 缓存(Top 100)
    文章目录前言1.描述2.示例3.答案关于我们前言本题为LeetCode前100高频题我们社区陆续会将顾毅(Netflix增长黑客,《iOS面试之道》作者,ACE职业健身教练。)的Swift算法题题解整理为文字版以方便大家学习与阅读。LeetCode算法到目前我们已经更新到145期......
  • 0基础读顶会论文(组会ppt版)-在Deviceless边缘计算环境中实现移动感知的无缝虚拟函数
    ......
  • CTF初探:揭秘信息安全的竞技舞台
    水一篇文章,介绍一下CTF该如何入门以及平时如何去学习练习(欢迎大家来一起交流学习)入门常用文档简介-CTFWiki(ctf-wiki.org)(推荐每一个方向都有介绍)‍‍⁤‌⁣⁢⁣⁡⁡⁡⁢⁤⁣⁡⁢⁢‍‬⁡⁡‬⁤‬⁣‌⁡‬‍⁡⁢‍⁣‍⁢入入入入门(fén)综述-飞书云文档(feis......
  • c4d适合做室内设计吗?c4d适用行业
    C4D这款功能强大的三维建模、动画和渲染软件,以其直观的界面和高效的工作流程而闻名。在室内设计领域,C4D展现出了其独特的优势,不仅能够快速创建逼真的室内场景,还能通过动态模拟和渲染技术,让设计师的创意得以生动呈现。本文说明C4D在室内设计中的应用,并概述其在多个行业中的适用性。......
  • 大数据学习记录,Python基础(2)
    数据类型字符串概述:由若干个字符组成字符序列,称之为字符串特点:字符串一旦被创建就不能被更改定义一个字符串s1="hello"字符串一旦被创建就不能被更改s1="hello"s1="world"#相当于将新的字符串内存中的地址值赋值给了s1,原本的"hello"的内容没有改变print(......