將 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 程式碼執行的設定) |