C#调用Python脚本完全指南


引言

在现代软件开发中,多语言协作已成为常态。C#作为强大的企业级开发语言,与Python这一数据科学和机器学习领域的主流语言相结合,可以发挥各自的优势。本文将全面介绍在C#应用程序中调用Python脚本的各种方法、技术细节和最佳实践。

一、基础调用方法

1. 使用Process类直接调用

using System.Diagnostics;

public class PythonRunner
{
    public static string ExecutePythonScript(string scriptPath, string arguments)
    {
        ProcessStartInfo start = new ProcessStartInfo();
        start.FileName = "python"; // 或指定完整路径如 @"C:\Python39\python.exe"
        start.Arguments = $"{scriptPath} {arguments}";
        start.UseShellExecute = false;
        start.RedirectStandardOutput = true;
        start.RedirectStandardError = true;
        start.CreateNoWindow = true;

        using (Process process = Process.Start(start))
        {
            using (StreamReader reader = process.StandardOutput)
            {
                string result = reader.ReadToEnd();
                string errors = process.StandardError.ReadToEnd();

                if (!string.IsNullOrEmpty(errors))
                {
                    throw new Exception($"Python error: {errors}");
                }

                return result;
            }
        }
    }
}

// 调用示例
string output = PythonRunner.ExecutePythonScript("script.py", "arg1 arg2");

2. 传递复杂参数和接收复杂结果

# script.py
import sys
import json

# 接收JSON格式参数
data = json.loads(sys.argv[1])
result = {
    "sum": data["a"] + data["b"],
    "product": data["a"] * data["b"]
}

print(json.dumps(result))
// C#调用代码
var parameters = new { a = 5, b = 7 };
string jsonArgs = JsonConvert.SerializeObject(parameters);
string jsonResult = PythonRunner.ExecutePythonScript("script.py", $"\"{jsonArgs}\"");

dynamic result = JsonConvert.DeserializeObject(jsonResult);
Console.WriteLine($"Sum: {result.sum}, Product: {result.product}");

二、高级集成方案

1. 使用IronPython (.NET实现的Python)

// 安装IronPython NuGet包
// Install-Package IronPython -Version 3.4.0

using IronPython.Hosting;
using Microsoft.Scripting.Hosting;

public class IronPythonService
{
    private readonly ScriptEngine engine;

    public IronPythonService()
    {
        engine = Python.CreateEngine();
    }

    public dynamic Execute(string script, Dictionary<string, object> scopeVariables = null)
    {
        ScriptScope scope = engine.CreateScope();

        if (scopeVariables != null)
        {
            foreach (var item in scopeVariables)
            {
                scope.SetVariable(item.Key, item.Value);
            }
        }

        return engine.Execute(script, scope);
    }

    public dynamic ExecuteFile(string filePath, Dictionary<string, object> scopeVariables = null)
    {
        ScriptScope scope = engine.CreateScope();

        if (scopeVariables != null)
        {
            foreach (var item in scopeVariables)
            {
                scope.SetVariable(item.Key, item.Value);
            }
        }

        return engine.ExecuteFile(filePath, scope);
    }
}

// 使用示例
var pyService = new IronPythonService();
try
{
    // 执行Python代码字符串
    int result = pyService.Execute("2 + 3 * 5");

    // 执行Python文件
    dynamic scriptResult = pyService.ExecuteFile("script.py", 
        new Dictionary<string, object> { ["input_param"] = 42 });

    Console.WriteLine(scriptResult);
}
catch (Exception ex)
{
    Console.WriteLine($"Python执行错误: {ex.Message}");
}

2. 使用Python.NET (pythonnet)

// 安装Python.NET NuGet包
// Install-Package Python.Runtime -Version 3.7.1

using Python.Runtime;

public class PythonNetService : IDisposable
{
    private IntPtr _pythonThreadState;

    public PythonNetService(string pythonHome = null)
    {
        if (!string.IsNullOrEmpty(pythonHome))
        {
            PythonEngine.PythonHome = pythonHome;
        }

        PythonEngine.Initialize();
        _pythonThreadState = PythonEngine.BeginAllowThreads();
    }

    public dynamic Execute(string pythonCode, Dictionary<string, object> variables = null)
    {
        using (Py.GIL()) // 获取Python全局解释器锁
        {
            dynamic scope = new PyDict();

            if (variables != null)
            {
                foreach (var item in variables)
                {
                    scope[item.Key] = item.Value.ToPython();
                }
            }

            return PythonEngine.Eval(pythonCode, scope);
        }
    }

    public dynamic ImportModule(string moduleName)
    {
        using (Py.GIL())
        {
            return Py.Import(moduleName);
        }
    }

    public void Dispose()
    {
        PythonEngine.EndAllowThreads(_pythonThreadState);
        PythonEngine.Shutdown();
    }
}

// 使用示例
using (var py = new PythonNetService(@"C:\Python38"))
{
    // 调用NumPy
    dynamic np = py.ImportModule("numpy");
    dynamic array = np.array(new List<float> { 1, 2, 3 });
    Console.WriteLine(array.mean());

    // 执行自定义脚本
    dynamic result = py.Execute("x + 1", new Dictionary<string, object> { ["x"] = 41 });
    Console.WriteLine(result);
}

三、性能优化策略

1. 保持Python进程长连接

public class PersistentPythonProcess : IDisposable
{
    private Process _process;
    private StreamWriter _stdin;
    private StreamReader _stdout;
    private StreamReader _stderr;

    public PersistentPythonProcess(string pythonPath = "python")
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = pythonPath,
            Arguments = "-i -u", // 交互模式,无缓冲
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true
        };

        _process = new Process { StartInfo = startInfo };
        _process.Start();

        _stdin = _process.StandardInput;
        _stdout = _process.StandardOutput;
        _stderr = _process.StandardError;
    }

    public string ExecuteCommand(string command)
    {
        _stdin.WriteLine(command);
        _stdin.WriteLine("print('---END---')"); // 标记命令结束
        _stdin.Flush();

        StringBuilder output = new StringBuilder();
        string line;
        while ((line = _stdout.ReadLine()) != "---END---")
        {
            output.AppendLine(line);
        }

        return output.ToString();
    }

    public void Dispose()
    {
        _stdin.WriteLine("quit()");
        _process.WaitForExit(1000);
        if (!_process.HasExited)
        {
            _process.Kill();
        }

        _stdin.Dispose();
        _stdout.Dispose();
        _stderr.Dispose();
        _process.Dispose();
    }
}

// 使用示例
using (var py = new PersistentPythonProcess())
{
    // 初始化环境
    py.ExecuteCommand("import math");

    // 多次调用避免启动开销
    for (int i = 0; i < 10; i++)
    {
        string result = py.ExecuteCommand($"math.sqrt({i})");
        Console.WriteLine(result);
    }
}

2. 使用gRPC或REST API构建微服务

# server.py - Flask示例
from flask import Flask, request, jsonify
import numpy as np

app = Flask(__name__)

@app.route('/calculate', methods=['POST'])
def calculate():
    data = request.json
    a = np.array(data['matrix_a'])
    b = np.array(data['matrix_b'])
    return jsonify({
        "dot_product": np.dot(a, b).tolist(),
        "determinant": np.linalg.det(a).tolist()
    })

if __name__ == '__main__':
    app.run(port=5000)
// C#客户端
public class PythonMicroserviceClient
{
    private readonly HttpClient _client;

    public PythonMicroserviceClient(string baseUrl)
    {
        _client = new HttpClient { BaseAddress = new Uri(baseUrl) };
    }

    public async Task<dynamic> CalculateMatrixAsync(double[][] matrixA, double[][] matrixB)
    {
        var request = new
        {
            matrix_a = matrixA,
            matrix_b = matrixB
        };

        var response = await _client.PostAsJsonAsync("/calculate", request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<ExpandoObject>();
    }
}

// 使用示例
var client = new PythonMicroserviceClient("http://localhost:5000");
var a = new double[][] { new[] { 1.0, 2.0 }, new[] { 3.0, 4.0 } };
var b = new double[][] { new[] { 5.0, 6.0 }, new[] { 7.0, 8.0 } };

var result = await client.CalculateMatrixAsync(a, b);
Console.WriteLine($"Determinant: {result.determinant}");

四、异常处理与调试

1. 综合异常处理

public class PythonExecutionException : Exception
{
    public string PythonStackTrace { get; }
    public string StandardError { get; }

    public PythonExecutionException(string message, string stackTrace, string stdError) 
        : base(message)
    {
        PythonStackTrace = stackTrace;
        StandardError = stdError;
    }
}

public static string ExecutePythonWithFullErrorHandling(string scriptPath)
{
    try
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = "python",
            Arguments = scriptPath,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        using (var process = Process.Start(startInfo))
        {
            string output = process.StandardOutput.ReadToEnd();
            string errors = process.StandardError.ReadToEnd();

            process.WaitForExit();

            if (process.ExitCode != 0)
            {
                throw new PythonExecutionException(
                    $"Python脚本执行失败(退出代码: {process.ExitCode})",
                    output, // Python traceback通常输出到stdout
                    errors);
            }

            return output;
        }
    }
    catch (Win32Exception ex) when (ex.NativeErrorCode == 2)
    {
        throw new Exception("Python解释器未找到", ex);
    }
    catch (Exception ex)
    {
        throw new Exception("执行Python脚本时发生错误", ex);
    }
}

2. 调试技巧

  1. 日志记录:记录完整的Python输入输出
   File.WriteAllText("python_input.txt", finalScript);
  1. 交互式调试
   // 在开发时使用交互模式
   startInfo.Arguments = "-i " + scriptPath;
  1. 可视化调试工具
  • 对gRPC/REST服务使用Postman或Insomnia
  • 对进程调用使用Process Monitor观察实际执行

五、部署注意事项

1. 依赖管理

# requirements.txt
numpy==1.21.2
pandas==1.3.3
scikit-learn==0.24.2
// 安装Python依赖
public static void InstallPythonDependencies(string requirementsPath = "requirements.txt")
{
    var startInfo = new ProcessStartInfo
    {
        FileName = "pip",
        Arguments = $"install -r \"{requirementsPath}\"",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    };

    using (var process = Process.Start(startInfo))
    {
        string output = process.StandardOutput.ReadToEnd();
        string errors = process.StandardError.ReadToEnd();

        process.WaitForExit();

        if (process.ExitCode != 0)
        {
            throw new Exception($"依赖安装失败: {errors}");
        }
    }
}

2. 打包Python环境

  1. 使用虚拟环境
   python -m venv myenv
  1. 嵌入式Python分发
  • 将Python嵌入到应用目录中
  • 修改PythonHome路径指向嵌入目录
  1. 使用PyInstaller打包
   pyinstaller --onefile script.py

结语

C#与Python的互操作性为开发者提供了强大的能力,结合了C#的企业级应用开发优势和Python丰富的数据科学生态系统。根据项目需求,您可以选择:

  1. 简单脚本调用:使用Process类直接调用
  2. 深度集成:采用IronPython或Python.NET
  3. 高性能场景:构建基于gRPC的微服务架构
  4. 复杂科学计算:利用Python.NET直接操作NumPy/Pandas对象

在实际项目中,建议:

  • 明确交互边界和数据格式
  • 实施完善的错误处理和日志记录
  • 考虑长期维护的便利性
  • 评估性能需求并选择合适的集成方案

通过合理利用这些技术,您可以构建出既强大又灵活的混合语言解决方案,充分发挥C#和Python各自的优势。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注