Csharp中调用Python之PythonNet简析

C# 中强大的 Python 桥梁:深入解析 pythonnet

1. 什么是 pythonnet

pythonnet(也称为 Python for .NET)是一个开源软件包,它为 .NET Framework、.NET Core 和 .NET 5+ 的公共语言运行时(CLR)与 CPython 解释器之间提供了近乎无缝的集成。

简单来说,pythonnet 就像一座桥梁,它允许 C# 代码:

  • 调用 Python 代码 :直接在 C# 项目中导入 Python 模块、创建 Python 对象、调用 Python 函数。
  • 被 Python 代码调用 :在 Python 脚本中加载 .NET 程序集,并使用 C# 编写的类和方法。

本文档将主要从 C# 调用 Python 的角度进行详细阐述。

pythonnet 的核心优势在于它 将 Python 解释器直接嵌入到 .NET 进程中 ,而不是通过进程间通信(如 Sockets、Pipes 或 REST API)。这意味着:

  • 高性能 :数据交换发生在内存中,避免了序列化和网络通信的开销,速度非常快。
  • 强大的互操作性 :.NET 对象和 Python 对象可以轻松地相互转换和传递。

官方 NuGet 包名: pythonnet

2. 核心原理

pythonnet 通过以下机制实现其强大的功能:

  1. 嵌入 CPython 解释器 :当你的 C# 程序初始化 pythonnet 时,它会加载指定路径的 Python 动态链接库(python3x.dll),并在当前进程内启动一个完整的 Python 解释器实例。
  2. 类型封送 (Type Marshalling)pythonnet 会自动处理 .NET 类型和 Python 类型之间的转换。例如:
    • C# string ↔ Python str
    • C# int, double ↔ Python int, float
    • C# List<T>, T[] ↔ Python list
    • C# Dictionary<K, V> ↔ Python dict
    • C# null ↔ Python None
  3. 全局解释器锁 (GIL) 管理 :Python 有一个著名的全局解释器锁(GIL),它确保在任何时候只有一个线程在执行 Python 字节码。pythonnet 提供了明确的机制来获取和释放这个锁,确保在多线程的 C# 环境中也能安全地与 Python 交互。

3. pythonnet 可以用来做什么?

pythonnet 的应用场景非常广泛,主要集中在利用 Python 庞大而成熟的生态系统来增强 C# 应用程序的能力。

  • 利用海量的 Python 库
    • 数据科学与机器学习 :在 C# 应用中直接使用 NumPyPandasScikit*learnTensorFlowPyTorch 等顶级库进行数据分析、模型训练和预测。
    • 数据可视化 :使用 MatplotlibSeaborn 在 C# 应用中生成复杂的图表和报告。
    • 图像处理 :利用 PillowOpenCV-Python 进行高级图像操作。
    • 网络爬虫 :在 C# 后端服务中集成 BeautifulSoupScrapy 来抓取网页数据。
    • 自然语言处理 (NLP) :使用 NLTKspaCy 进行文本分析。
  • 集成现有的 Python 脚本和代码库
    如果你的团队或公司已经有大量成熟的 Python 算法、模型或工具脚本,无需用 C# 重写,可以直接通过 pythonnet 集成到新的 .NET 项目中。
  • 快速原型验证
    Python 通常被认为在某些领域(如数据分析)比 C# 更具表现力和简洁性。你可以先用 Python 快速实现一个算法原型,然后在 C# 主程序中调用它进行验证和集成。
  • 自动化与脚本任务
    利用 Python 强大的脚本能力,为 C# 桌面应用或后台服务添加灵活的自动化任务功能。

4. 如何开始使用 pythonnet

步骤 1: 环境准备

  1. 安装 .NET 环境 :确保你已安装 .NET SDK(例如 .NET 6 或 .NET 8)。可以使用 Visual Studio 或 VS Code。
  2. 安装 Python
    • Python 官网 下载并安装 Python(推荐 3.8 - 3.11 版本,pythonnet 对最新版本的支持可能稍有延迟)。
    • 重要 :在安装时,务必勾选 “Add Python to PATH”。
    • 关键 :确保你的 C# 应用程序的目标平台架构(x64 或 x86)与安装的 Python 解释器的架构一致。现在绝大多数情况都应使用 64 位版本。

步骤 2: 安装 NuGet 包

在你的 C# 项目中,通过 NuGet 包管理器安装 pythonnet

dotnet add package pythonnet

步骤 3: 安装所需的 Python 包

打开命令行或终端,使用 pip 安装你将要在 C# 中调用的 Python 库。例如,我们将使用 numpymatplotlib

pip install numpy
pip install matplotlib

步骤 4: 编写 C# 初始化代码

这是使用 pythonnet 的标准"样板代码"。

using Python.Runtime;
using System;

public class PythonManager
{
    public static void Initialize()
    {
        // 设置 Python DLL 的路径
        // 如果 Python 已添加到环境变量 PATH,通常可以省略这一步。
        // 但为了程序的健壮性,显式指定更好。
        // 例如: Runtime.PythonDLL = @"C:\Python39\python39.dll";
        
        PythonEngine.Initialize();
        
        // 当程序退出时,确保关闭 Python 引擎
        AppDomain.CurrentDomain.ProcessExit += (s, e) =>
        {
            if (PythonEngine.IsInitialized)
            {
                PythonEngine.Shutdown();
            }
        };
    }
}

// 在你的主程序入口处调用初始化
public class Program
{
    public static void Main(string[] args)
    {
        PythonManager.Initialize();

        // 在这里编写你的 Python 调用代码...

        Console.WriteLine("按任意键退出...");
        Console.ReadKey();
    }
}

关于 using (Py.GIL())

所有与 Python 对象的交互都必须在获取了 Python 全局解释器锁(GIL)的代码块中进行。pythonnet 提供了 using (Py.GIL()) 语法糖来简化这个过程。它能确保在代码块执行前获取锁,在代码块结束后(即使发生异常)自动释放锁。

using (Py.GIL()) // <-- 这是关键!
{
    // 所有 Python 相关的操作都应放在这里
}

5. 详细代码示例

下面是一些从易到难的具体示例。

示例 1: 执行简单的 Python 代码

这个例子展示了如何直接运行一行 Python 代码和一小段脚本。

using Python.Runtime;
using System;

public static void RunSimplePython()
{
    using (Py.GIL())
    {
        // 1. 执行单行 Python 表达式
        Console.WriteLine("--- 执行单行代码 ---");
        dynamic np = Py.Import("numpy"); // 动态导入 numpy 模块
        Console.WriteLine($"Numpy 版本: {np.__version__}");

        // 2. 执行多行 Python 脚本
        Console.WriteLine("\n--- 执行多行脚本 ---");
        using (var scope = Py.CreateScope())
        {
            // 定义一个 Python 脚本
            string code = @"
import sys
print(f'Python 版本: {sys.version}')
x = 10
y = 20
result = x + y
";
            // 在指定作用域内执行脚本
            scope.Exec(code);
            
            // 从作用域中获取 Python 变量到 C#
            int result = scope.Get<int>("result");
            Console.WriteLine($"从 Python 获取的计算结果: {result}"); // 输出 30
        }
    }
}

// 在 Main 函数中调用:
// RunSimplePython();

示例 2: 调用自定义 Python 脚本中的函数

这是更常见的用例:调用一个你自己编写的 .py 文件中的函数。

1. 创建 Python 脚本 (my_script.py):

在你的项目目录下(或任何 C# 能访问到的地方)创建一个名为 my_script.py 的文件:

# my_script.py
import datetime

def greet(name):
    """一个简单的问候函数"""
    return f"Hello, {name}! The time is {datetime.datetime.now()}."

def add_list(items):
    """计算列表中所有数字的和"""
    print(f"在 Python 中接收到的列表类型: {type(items)}")
    return sum(items)

2. 在 C# 中调用该脚本的函数:

using Python.Runtime;
using System;
using System.Collections.Generic;

public static void CallCustomScript()
{
    using (Py.GIL())
    {
        // 将脚本所在的目录添加到 Python 的 sys.path 中
        // 这样 Python 解释器才能找到 'my_script' 模块
        dynamic sys = Py.Import("sys");
        sys.path.append("."); // "." 表示当前目录

        Console.WriteLine("--- 调用自定义 Python 脚本 ---");

        // 动态导入你的脚本,就像导入一个普通模块一样
        dynamic myScript = Py.Import("my_script");

        // 1. 调用 greet 函数,并传递一个 C# 字符串
        string name = "C# Developer";
        string greeting = myScript.greet(name);
        Console.WriteLine($"来自 Python 的问候: {greeting}");

        // 2. 调用 add_list 函数,并传递一个 C# 列表
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        // PyObject 会被自动封送为 Python list
        dynamic result = myScript.add_list(numbers); 
        
        // 将 Python 返回的对象转换为 C# 类型
        int sum = result.As<int>(); 
        Console.WriteLine($"Python 计算的列表总和: {sum}");
    }
}

// 在 Main 函数中调用:
// CallCustomScript();

示例 3: 使用 NumPy 和 Pandas 进行数据处理

这个例子展示了 pythonnet 的核心价值——利用强大的数据科学库。

using Python.Runtime;
using System;

public static void UseNumpyAndPandas()
{
    using (Py.GIL())
    {
        Console.WriteLine("\n--- 使用 NumPy ---");
        dynamic np = Py.Import("numpy");

        // 从 C# 数组创建 NumPy 数组
        var data = new double[] { 1.0, 2.5, 3.0, 4.5, 5.0 };
        dynamic npArray = np.array(data);
        Console.WriteLine($"创建的 NumPy 数组: {npArray}");

        // 调用 NumPy 函数
        dynamic mean = np.mean(npArray);
        dynamic std = np.std(npArray);

        Console.WriteLine($"平均值: {mean.As<double>()}");
        Console.WriteLine($"标准差: {std.As<double>()}");

        Console.WriteLine("\n--- 使用 Pandas ---");
        dynamic pd = Py.Import("pandas");

        // 创建一个 Python 字典来构建 DataFrame
        var dict = new PyDict();
        dict["Name"] = new PyList(new[] { "Alice", "Bob", "Charlie" }.ToPython());
        dict["Age"] = new PyList(new[] { 25, 30, 35 }.ToPython());
        
        // 创建 Pandas DataFrame
        dynamic df = pd.DataFrame(dict);
        Console.WriteLine("创建的 Pandas DataFrame:");
        Console.WriteLine(df);

        // 获取 'Age' 列的平均值
        dynamic ageMean = df["Age"].mean();
        Console.WriteLine($"\n平均年龄: {ageMean.As<double>()}");
    }
}

// 在 Main 函数中调用:
// UseNumpyAndPandas();

示例 4: 使用 Matplotlib 进行数据可视化

你甚至可以从 C# 调用 Python 的绘图库来生成图表。

using Python.Runtime;
using System;

public static void UseMatplotlib()
{
    using (Py.GIL())
    {
        Console.WriteLine("\n--- 使用 Matplotlib 绘图 ---");
        dynamic np = Py.Import("numpy");
        dynamic plt = Py.Import("matplotlib.pyplot");

        // 生成数据
        dynamic x = np.linspace(0, 2 * np.pi, 100);
        dynamic y = np.sin(x);

        // 绘图
        plt.plot(x, y);
        plt.title("Sin Wave from C# via Matplotlib");
        plt.xlabel("X-axis");
        plt.ylabel("Y-axis");
        
        // 显示图表
        // 这会弹出一个绘图窗口,就像在 Python 中一样
        Console.WriteLine("正在显示图表窗口...");
        plt.show();
        Console.WriteLine("图表窗口已关闭。");
    }
}

// 在 Main 函数中调用:
// UseMatplotlib();

6. 高级主题与最佳实践

  • 错误处理
    Python 异常会在 C# 中被捕获为 Python.Runtime.PythonException。你可以通过 try-catch 块来处理它们。

    try
    {
        using (Py.GIL())
        {
            dynamic nonExistentModule = Py.Import("non_existent_module");
        }
    }
    catch (PythonException ex)
    {
        Console.WriteLine($"捕获到 Python 异常: {ex.Message}");
        Console.WriteLine($"Python 异常类型: {ex.Type}");
        Console.WriteLine($"Python 堆栈跟踪: {ex.StackTrace}");
    }
    
  • 类型转换

    • 使用 dynamic 关键字最方便,但没有编译时类型检查。
    • 使用 PyObject 类型更明确,它代表了任何 Python 对象的引用。
    • 使用 .As<T>().ToManaged<T>() 方法将 PyObjectdynamic 对象显式转换回 C# 类型。
  • 性能考虑
    虽然 pythonnet 很快,但在 C# 和 Python 之间传递大量数据(例如巨大的数组)仍然有开销。对于性能极其敏感的循环,最好将整个循环逻辑放在 Python 端执行,只在 C# 中传递最终结果。

  • 部署
    部署一个使用了 pythonnet 的应用程序是最具挑战性的部分。你需要确保目标机器上:

    1. 安装了正确版本和架构的 Python。
    2. 安装了所有必需的 Python 包。
    3. 你的 C# 程序能够找到 pythonXX.dll
      解决方案 :考虑使用包含嵌入式 Python 发行版的打包工具(如 pythonnet_netstandard_embed),或者编写一个安装脚本来为用户配置好 Python 环境。

部署,详见下文 自包含部署策略 部分内容。

7. 总结

pythonnet 是一个极其强大的工具,它为 .NET 开发者打开了通往整个 Python 生态系统的大门。它使得在 C# 应用程序中利用 Python 在数据科学、机器学习、自动化等领域的优势变得轻而易举。

优点 :

  • 无缝集成高性能
  • 能够直接利用 数以万计的成熟 Python 库
  • 节省开发时间 ,无需用 C# 重复造轮子。

挑战 :

  • 环境配置和部署 相对复杂,需要仔细管理 Python 依赖。
  • 需要对 Python 的 全局解释器锁 (GIL) 有基本了解。

总的来说,如果你需要在 C# 项目中实现 Python 能够轻松完成的功能,pythonnet 绝对是你的首选方案。

当然可以。你提出的这个问题正是使用 pythonnet 时最关键也是最棘手的一环。一个理想的桌面或服务器应用程序应该对最终用户透明,让他们无需关心背后依赖的 Python 环境。

pythonnet 应用程序的自包含部署策略

1. 问题的核心:为什么部署困难?

如上文中所说,一个典型的 pythonnet 应用有三个主要的外部依赖:

  1. .NET 运行时 :除非用户已安装,否则需要。
  2. Python 解释器 :你的代码需要一个特定版本和架构(x64/x86)的 Python 环境。
  3. Python 包 :你的代码依赖的库,如 numpy, pandas 等。

“自包含部署”的目标就是将这三者(或至少后两者)与你的应用程序代码打包在一起,形成一个独立的文件夹或安装包。

2. 核心理念:将 Python 环境“嵌入”你的应用

我们的策略是将一个迷你的、可移植的 Python 环境直接放到你的应用程序发布目录中,并配置 pythonnet 使用这个内部的 Python,而不是系统上全局安装的 Python。

这里主要介绍两种主流且可靠的方法:

  • 方法一:手动集成 Python 嵌入版(Embeddable Package) - 更灵活,控制力更强。
  • 方法二:使用 pythonnet_pyembed NuGet 包 - 更简单,自动化程度更高。

对于大多数新项目,强烈推荐方法二,因为它大大简化了流程。

方法一:手动集成 Python 嵌入版

这种方法让你对 Python 的版本和内容有完全的控制。

步骤 1: 下载 Python 嵌入版

  1. 前往 Python 官网下载页面
  2. 找到你需要的 Python 版本(例如 Python 3.9.13)。
  3. 在文件列表中,下载 Windows embeddable package (64-bit)。这是一个 .zip 文件。

步骤 2: 在项目中组织 Python 环境

  1. 在你的 C# 项目的根目录下,创建一个名为 python-env 的文件夹。

  2. 将下载的 .zip 文件解压到这个 python-env 文件夹中。解压后,你的目录结构看起来像这样:

    YourSolution/
    └── YourProject/
        ├── YourProject.csproj
        ├── Program.cs
        └── python-env/      <-- 新建的文件夹
            ├── python.exe
            ├── python39.dll   <-- 这是关键的 DLL
            ├── python39._pth
            ├── ... (其他文件)
    

步骤 3: 为嵌入版安装 pip 和依赖包

默认情况下,嵌入版不包含 pip。我们需要手动安装它。

  1. 修改 ._pth 文件 :打开 python-env 目录下的 python39._pth (文件名可能因版本而异)。这个文件定义了 Python 的模块搜索路径。默认内容可能只有 .python39.zip。在文件末尾添加一行 Lib/site-packages 并取消 import site 的注释(删除前面的 #):

    python39.zip
    .
    # Uncomment to run site.main() automatically
    import site
    

    修改为:

    python39.zip
    .
    Lib/site-packages
    # Uncomment to run site.main() automatically
    import site
    

    (注:较新版本的 embeddable 包可能需要取消注释 import site 才能让 site-packages 生效)

  2. 下载 get-pip.py :打开终端,进入 python-env 目录,然后使用 curl 或浏览器下载 get-pip.py 脚本。

    cd path/to/your/project/python-env
    curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
    
  3. 安装 pip :运行 python.exe 来执行 get-pip.py

    ./python.exe get-pip.py
    

    执行完毕后,你会在 python-env 目录下看到 LibScripts 两个新文件夹。pip.exe 就在 Scripts 里面。

  4. 安装 Python 包 :现在,使用刚刚安装的 pip 来安装你的依赖。

    ./Scripts/pip.exe install numpy pandas scikit-learn
    

    所有这些包都会被安装到 python-env/Lib/site-packages 目录下。

步骤 4: 修改 C# 代码以使用嵌入式 Python

现在,你需要告诉 pythonnet 去哪里找 Python DLL。

using Python.Runtime;
using System;
using System.IO;
using System.Reflection;

public static class PythonManager
{
    public static void Initialize()
    {
        // 获取应用程序的基目录
        string assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        // 拼接嵌入式 Python 环境的路径
        string pythonEnvPath = Path.Combine(assemblyPath, "python-env");
        string pythonDll = Path.Combine(pythonEnvPath, "python39.dll"); // 根据你的版本修改

        // 设置环境变量,以便 Python 能找到自己的库
        Environment.SetEnvironmentVariable("PYTHONNET_PYDLL", pythonDll);
        Environment.SetEnvironmentVariable("PYTHONHOME", pythonEnvPath);
        Environment.SetEnvironmentVariable("PYTHONPATH", $"{pythonEnvPath}\\Lib;{pythonEnvPath}\\Lib\\site-packages");

        PythonEngine.Initialize();
        
        AppDomain.CurrentDomain.ProcessExit += (s, e) =>
        {
            if (PythonEngine.IsInitialized)
            {
                PythonEngine.Shutdown();
            }
        };
    }
}

注意 :通过设置 PYTHONNET_PYDLL 环境变量是 pythonnet 推荐的定位 DLL 的方式,比直接设置 Runtime.PythonDLL 更健壮。

步骤 5: 配置项目以在发布时包含 Python 环境

你需要让 dotnet 在构建和发布时,将整个 python-env 文件夹复制到输出目录。编辑你的 .csproj 文件,添加以下 ItemGroup

<Project Sdk="Microsoft.NET.Sdk">

  <!-- ... 其他属性组 ... -->

  <ItemGroup>
    <Content Include="python-env\**">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="pythonnet" Version="3.0.3" />
  </ItemGroup>

</Project>

PreserveNewest 意味着只有在文件有更新时才复制,可以加快后续构建速度。

步骤 6: 发布自包含应用

现在,使用 dotnet publish 命令来创建最终的可分发版本。

# -c Release: 使用 Release 配置
# --self-contained: 将 .NET 运行时也打包进去
# -r win-x64: 指定目标平台 (Runtime Identifier)。这是必须的!
dotnet publish -c Release --self-contained -r win-x64

发布完成后,在 bin/Release/netX.X/win-x64/publish 目录下,你会找到一个完整的、可以被复制到任何(64位Windows)电脑上运行的文件夹。

方法二:使用 pythonnet_pyembed NuGet 包(推荐)

这个方法利用了一个专门为此场景设计的 NuGet 包,它会自动下载嵌入式 Python 并管理依赖。

步骤 1: 安装 NuGet 包

在你的项目中,卸载 pythonnet(如果已安装),然后安装 pythonnet_pyembed。它会自动依赖并引入 pythonnet

dotnet add package pythonnet_pyembed

这个包本身不包含 Python,它是一个“引导”包。它会根据你的配置,在构建时下载指定的 Python 版本。

步骤 2: 配置 Python 版本和依赖项

在你的项目根目录,创建一个名为 python.props 的文件(或者直接在 .csproj 中添加)。这个文件将告诉 pythonnet_pyembed 需要哪个版本的 Python 和哪些包。

python.props 文件示例:

<Project>
  <PropertyGroup>
    <!-- 指定需要的 Python 版本。构建时会自动下载这个版本的嵌入包 -->
    <PythonVersion>3.9.13</PythonVersion>
  </PropertyGroup>

  <ItemGroup>
    <!-- 列出所有需要的 Python 包和它们的版本 -->
    <PythonPackage Include="numpy" Version="1.24.3" />
    <PythonPackage Include="pandas" Version="2.0.1" />
    <PythonPackage Include="scikit-learn" Version="1.2.2" />
  </ItemGroup>
</Project>

步骤 3: 修改 C# 代码

使用这个方法,你的 C# 初始化代码会变得非常简单,因为 pythonnet_pyembed 会自动处理 DLL 的定位。你 不需要 再手动设置 Runtime.PythonDLL 或任何环境变量。

using Python.Runtime;
using System;

public static class PythonManager
{
    public static void Initialize()
    {
        // 无需任何路径设置!
        // `pythonnet_pyembed` 在后台已经处理好了
        PythonEngine.Initialize();
        
        AppDomain.CurrentDomain.ProcessExit += (s, e) =>
        {
            if (PythonEngine.IsInitialized)
            {
                PythonEngine.Shutdown();
            }
        };
    }
}

步骤 4: 发布自包含应用

发布过程与方法一完全相同。

dotnet publish -c Release --self-contained -r win-x64

当你第一次构建或发布时,NuGet 会自动下载指定的 Python 嵌入版,并使用 pip 安装你在 python.props 中声明的所有包。这一切都是自动完成的!最终,publish 文件夹也会包含完整的 Python 环境。

方法对比

特性 手动集成 (方法一) pythonnet_pyembed (方法二)
易用性 复杂,步骤繁琐 非常简单 ,配置驱动
自动化 低,需要手动下载和安装 ,构建过程自动完成所有操作
灵活性 极高 ,可以对 Python 环境做任何修改 较高,通过 props 文件配置
可重复性 较低,依赖手动操作的准确性 极高 ,定义在代码和配置文件中
推荐场景 需要高度定制 Python 环境,或在无法访问 NuGet 的离线环境中使用 绝大多数项目 ,尤其是 CI/CD 环境

重要注意事项

  • 应用体积 :自包含部署会显著增加你的应用体积。一个包含 .NET 运行时和基本数据科学包的 Python 环境,发布后的文件夹大小可能在 300MB - 1GB 之间。
  • 平台特定 :自包含发布是平台相关的。如果你为 win-x64 发布,它就只能在 64 位 Windows 上运行。若要支持 Linux,你需要使用 linux-x64 的 RID 重新发布 (dotnet publish -r linux-x64)。
  • 许可证 :当你分发你的应用时,你也同时在分发 Python 和所有 Python 包。请确保你遵守了 Python Software Foundation License 以及你使用的所有第三方库的许可证(如 MIT, BSD, Apache 等)。

通过以上方法,你就可以自信地将你的 pythonnet 应用交付给最终用户,而无需他们进行任何复杂的技术配置。

友情链接

Csharp中的Dynamic类型简析 - 编程技术 - CEPD@BBS