将 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 代码运行的设置) |