繁體中文
English 简体中文 繁體中文
Methodox 首頁

將 SteelPython 嵌入到 C# 中

.NET 應用程式可以在內部攜帶 Python:執行使用者編寫的指令碼、呼叫 Python 庫,或讓 Python 程式碼驅動應用程式自身的物件。使用 SteelPython 時,直譯器是你程序中的一個 .NET 庫——無需在使用者的機器上安裝任何東西,不需要單獨的 Python,也不需要在兩個執行時之間進行封送:Python 物件和 C# 物件直接相互傳遞。

嵌入功能使用 SteelPython NuGet 包,它是 SteelPython SDK 的一部分,面向企業使用者提供(請參閱 取得 SteelPython)。

第一個應用程式

在 .NET 10 專案中引用該包:

<ItemGroup>
  <PackageReference Include="SteelPython" Version="0.1.0" />
</ItemGroup>
using SteelPython.Hosting;
using SteelPython.Interop;

using var engine = PythonEngine.Initialize();   // finds the standard library next to the application
engine.Expose<Order>(module: "shop");            // Python can now: from shop import Order

engine.Execute("""
    from shop import Order

    def summarize(orders):
        total = sum(o.Quantity * o.Price for o in orders)
        return f"{len(orders)} orders, {total:.2f} in total"
    """);

var orders = new List<Order> { new("bolts", 900, 0.12m), new("nuts", 1200, 0.08m) };
Console.WriteLine(engine.Main["summarize"].Call(orders).As<string>());   // 2 orders, 204.00 in total

[PythonExport]
public record Order(string Item, int Quantity, decimal Price);

用 dotnet run 執行它。該包會在構建時把 Python 標準庫複製到應用程式旁邊,因此輸出資料夾是完整的,可以複製到任何地方。

引擎

PythonEngine.Initialize() 啟動直譯器;釋放引擎時會執行 Python 的 atexit 處理程式並將其關閉。每個程序只有一個引擎;請使用作用域(見下文)將不同的指令碼彼此隔開。

var options = new PythonEngineOptions
{
    Stdout = myWriter,          // sys.stdout (also Stderr and Stdin): any TextWriter / TextReader
};
options.SearchPaths.Add(Path.Combine(AppContext.BaseDirectory, "scripts"));   // added to the front of sys.path
options.Argv.Add("myapp");                                                     // sys.argv
using var engine = PythonEngine.Initialize(options);

執行程式碼並取得值

engine.Execute("import math\nr = math.sqrt(2)");      // statements, in the __main__ module
double r = engine.Main["r"].As<double>();
int n = engine.Evaluate<int>("len('SteelPython')");     // an expression: 11

PythonObject math = engine.Import("math");              // import a module
double root = math.Invoke<double>("sqrt", 2.0);         // call one of its functions

engine.Execute("import greet");                         // greet.py, from a folder in SearchPaths
string hello = engine.Evaluate<string>("greet.hello('C#')");

PythonObject 是指向任意 Python 物件的控制代碼。它提供 Call(...)(實參會自動從 C# 轉換;接受關鍵字實參字典的是第二個過載)、用於呼叫方法的 Invoke(name, ...)、GetAttr/SetAttr、用於 obj[key] 的索引器、Length、Contains、用 foreach 迭代,以及型別轉換:As<T>()、AsList<T>()、AsDictionary<K, V>()。ToString() 相當於 Python 的 str(),Repr() 相當於其 repr()。

作用域

作用域是一個獨立的全域名稱空間,因此各指令碼看不到彼此的變數:

var scope = engine.CreateScope();
scope.Set("price", 19.99m);
engine.Execute("with_tax = price * 2", scope);
decimal withTax = scope.Get<decimal>("with_tax");     // 39.98
scope.ExecuteFile("rules.py");

在一個作用域中匯入的模組會與其他作用域共享,就像在同一個 Python 程式中一樣;如需完全隔離,請使用不同的程序。

錯誤

Python 異常到達 C# 時是一個 PythonException:

try { engine.Execute("1 / 0"); }
catch (PythonException e)
{
    Console.WriteLine($"{e.PythonType}: {e.PythonMessage}");   // ZeroDivisionError: division by zero
    Console.WriteLine(e.PythonTraceback);                       // formatted as Python prints it
}

讓 Python 存取你的應用程式

engine.Expose<T>() 會讓一個 C# 型別變得可匯入,預設從以其名稱空間命名的模組中匯入;給該型別標記 [PythonExport],即可獲得在編譯時生成的快速繫結(相關特性)。對於小型功能,可以在程式碼中構建一個模組:

engine.RegisterModule("host", m =>
{
    m.Function("tax_rate", (string region) => region == "EU" ? 0.2 : 0.1);
    m.Action("log", (string message) => logger.Info(message));
    m.Value("VERSION", "1.0");
});
import host
host.log(f"rate is {host.tax_rate('EU')}")

將 Python 物件用作 C# 介面

給介面標記 [PythonInterface] 後,任何具有匹配方法和屬性的 Python 物件都可以在 C# 中作為該介面使用:

[PythonInterface]
public interface IPricingRule
{
    string Name { get; }
    decimal Apply(decimal total);
}

IPricingRule rule = engine.Evaluate("make_rule()").As<IPricingRule>();
decimal discounted = rule.Apply(10m);       // calls the Python method

這是外掛的自然形態:應用程式定義介面,使用者編寫實現該介面的 Python 類。Python 類也可以派生自標記為 [PythonSubclassable] 的 C# 類,並重寫其虛方法(示例)。

非同步

Python 協程會變成 .NET 任務,.NET 任務也可以在 Python 中被 await:

engine.Execute("""
    async def fetch(n):
        import asyncio
        await asyncio.sleep(0.01)
        return n * 2
    """);
int doubled = await engine.Main["fetch"].Call(21).AsTask<int>();   // 42

AsTask 會在引擎於後臺執行緒上維護的 asyncio 事件迴圈中執行協程,並接受一個 CancellationToken,用於取消 Python 任務。AsAsyncEnumerable<T>() 會將非同步生成器轉換為 IAsyncEnumerable<T>。

執行緒

沒有全域直譯器鎖:可以同時從多個執行緒使用引擎,從不同執行緒呼叫的 Python 程式碼會並行執行。Python 關於共享狀態的常規規則依然適用——請用鎖保護複合更新。

包與擴充模組

用 SteelPython 的 pip 將包安裝到某個資料夾中,然後把該資料夾新增到 SearchPaths:

spy -m pip install --target python-packages requests pandas
options.SearchPaths.Add(Path.Combine(AppContext.BaseDirectory, "python-packages"));

帶有 C 擴充的包(numpy、pandas 等)在嵌入式引擎中同樣可用:NuGet 包還會把 SteelPython 為它們所需的原生部分複製到應用程式旁邊。

釋出

dotnet publish 會生成一個資料夾,其中包含你的應用程式、SteelPython 執行時和標準庫。SteelPython 也可以在用 Native AOT 編譯的應用程式中使用(dotnet publish -r win-x64 -p:PublishAot=true),這類應用程式啟動更快,目標機器上也不需要 .NET 執行時;在這種情況下,Python 可以存取應用程式公開的 C# 型別(通過 [PythonExport] 及其他特性),但無法使用 clr 對任意 .NET 型別的反射。

專案屬性 效果
<SteelPythonCopyStdlib>false</SteelPythonCopyStdlib> 不復制標準庫;改為將 PythonEngineOptions.HomePath 設定為包含 Lib\ 和 lib_steel\ 的資料夾
<SteelPythonCopyCApiShim>false</SteelPythonCopyCApiShim> 不復制 C 擴充模組所需的原生部分
<SteelPythonTuneRuntime>false</SteelPythonTuneRuntime> 保留你專案自己的 .NET 垃圾回收器設定(預設情況下,該包會應用最適合 Python 程式碼執行的設定)