编写扩展
Python 代码可以调用编译代码,以提高速度,或者使用并非用 Python 编写的库。SteelPython 支持两种扩展:
| C 扩展 | C# 扩展 | |
|---|---|---|
| 编写语言 | C、C++、Cython、Rust(PyO3)……——任何面向 CPython C API 的语言 | C#(或任何 .NET 语言) |
| 构建工具 | C 编译器(Windows 上为 MSVC),通过 setuptools 和 pip | .NET SDK(dotnet build) |
| 加载方式 | import name,与任何模块相同 |
同样是 import name:将 .dll 放在 sys.path 上,就像扩展模块一样 |
| 是否也能在 CPython 上运行 | 是:同一份源代码可以为两者构建 | 否(CPython 需要 pythonnet) |
| 最适合 | 封装 C 库;同样需要在 CPython 上运行的代码;现有的扩展包 | .NET 库和服务;无需 C 工具链的快速代码;与 C# 应用程序共享类型 |
他人发布的扩展完全不需要你做任何工作:带有 C 扩展的 wheel 会像在 CPython 中一样从 PyPI 安装(包与环境)。
用 C 编写扩展
SteelPython 可以运行基于 CPython 3.12 C API 编写的扩展模块,因此官方指南完全适用:扩展和嵌入 Python 解释器,以及 C API 参考。受限 API 和稳定 ABI(Py_LIMITED_API、abi3 wheel)同样可用。Cython、pybind11、PyO3 和 mypyc 生成的代码使用的也是同一套 API,用它们中任何一个构建的包都可以在 SteelPython 上运行。
教程:用 C 编写一个模块
你需要 Microsoft C++ Build Tools(Desktop development with C++ 工作负载),这与 CPython 的要求相同。SteelPython 提供头文件(Include\)和导入库(libs\);你无需为它们指定任何路径。
创建一个名为 fastgeo 的文件夹,其中包含三个文件。
src/fastgeo.c:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <math.h>
/* distance(x1, y1, x2, y2) -> float */
static PyObject *
distance(PyObject *self, PyObject *args)
{
double x1, y1, x2, y2;
if (!PyArg_ParseTuple(args, "dddd", &x1, &y1, &x2, &y2))
return NULL;
return PyFloat_FromDouble(hypot(x2 - x1, y2 - y1));
}
static PyMethodDef methods[] = {
{"distance", distance, METH_VARARGS, "Distance between two points."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "fastgeo", "Geometry helpers written in C.", -1, methods
};
PyMODINIT_FUNC
PyInit_fastgeo(void)
{
return PyModule_Create(&module);
}
setup.py:
from setuptools import setup, Extension
setup(ext_modules=[Extension("fastgeo", ["src/fastgeo.c"])])
pyproject.toml:
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "fastgeo"
version = "0.1.0"
将它构建并安装到虚拟环境中,然后使用它:
spy -m venv .venv
.venv\Scripts\activate
spy -m pip install ./fastgeo
spy -c "import fastgeo; print(fastgeo.distance(0, 0, 3, 4))"
5.0
pip 编译出了 fastgeo.cp312-win_amd64.pyd,与 CPython 3.12 使用的文件名相同。若要分享它,请用 spy -m pip wheel ./fastgeo -w dist 构建一个 wheel。
注意事项
- 引用计数按 C API 文档所述工作:保持
Py_INCREF/Py_DECREF配对平衡,就像在 CPython 中一样。 - GIL:SteelPython 没有 GIL,但 C API 中与之相关的调用(
PyGILState_Ensure、Py_BEGIN_ALLOW_THREADS等)都可以使用,正确使用这些调用的扩展在 SteelPython 上也是正确的。Python 线程确实会并行运行,因此保存全局状态、并依赖 GIL 来串行化对其调用的 C 代码,应当用自己的锁来保护这些状态,这与 CPython 自由线程构建的要求相同。 - CPython 内部结构:请使用 C API,而不要直接读取解释器的结构。包含 CPython
internal/头文件、或读取帧和解释器状态字段的代码不受支持。 - 速度:每次从 Python 调用 C,以及每次反向调用 C API,开销都比在 CPython 上略高。每次调用完成适量的工作(在 C 中循环处理整个数组,而不是每个元素调用一次),这种差异就会消失。
用 C# 编写扩展
.NET 类库就是 SteelPython 的扩展。分为两个层次。
任意类库
.NET 程序集中的每个公共类型都可以在 Python 中使用,无需特别准备。一个类库:
// Units.cs, in a project made with: dotnet new classlib -n Units
namespace Units;
public enum Unit { Metre, Foot, Inch }
public static class Converter
{
public static double ToMetres(double value, Unit unit) => unit switch
{
Unit.Foot => value * 0.3048,
Unit.Inch => value * 0.0254,
_ => value,
};
}
构建它(dotnet build -c Release),把 Units.dll 放在脚本旁边,然后:
from Units import Converter, Unit # finds Units.dll on sys.path: here, the script's folder
print(Converter.ToMetres(10, Unit.Foot)) # 3.048
它的类型从其 C# 命名空间导入,行为如 在 Python 中使用 .NET 中所述。
专为 Python 设计的库
借助 SteelPython NuGet 包(SteelPython SDK 的一部分),库可以选择其类型所在的 Python 模块,获得在编译时生成的绑定(调用更快,并且可用于 Native AOT),还可以提供供 Python 代码继承的类。引用该包并标记类型:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<!-- a library does not need its own copy of the standard library -->
<SteelPythonCopyStdlib>false</SteelPythonCopyStdlib>
<SteelPythonCopyCApiShim>false</SteelPythonCopyCApiShim>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SteelPython" Version="0.1.0" />
</ItemGroup>
</Project>
using SteelPython.Interop;
namespace Geometry;
[PythonExport(Module = "geometry")] // import geometry; geometry.Vector
public class Vector
{
public Vector(double x, double y) { X = x; Y = y; }
public double X { get; set; }
public double Y { get; set; }
public double Length => Math.Sqrt(X * X + Y * Y);
public Vector Scale(double factor = 2) => new(X * factor, Y * factor);
public static Vector operator +(Vector a, Vector b) => new(a.X + b.X, a.Y + b.Y);
public override string ToString() => $"Vector({X}, {Y})";
}
[PythonSubclassable(Module = "geometry")] // Python classes may derive from it
public abstract class Shape
{
public abstract double Area();
public virtual string Describe() => $"a shape of area {Area():0.##}";
public static double TotalArea(IEnumerable<Shape> shapes) => shapes.Sum(s => s.Area());
}
[PythonExport(Module = "geometry")]
public static class Survey
{
public static async Task<int> CountAsync(int n)
{
await Task.Delay(10);
return n;
}
}
在 Python 中:
import asyncio
from geometry import Vector, Shape, Survey # Geometry.dll, next to the script; the module its attributes name
v = Vector(3, 4)
print(v.Length, v + Vector(1, 1), v.Scale(), v.Scale(factor=0.5))
# 5.0 Vector(4, 5) Vector(6, 8) Vector(1.5, 2)
class Square(Shape): # a Python class deriving from the C# class
def __init__(self, side):
super().__init__()
self.side = side
def Area(self): # overrides the abstract method
return self.side ** 2
def Describe(self):
return "a square, " + super().Describe()
print(Shape.TotalArea([Square(2), Square(3)])) # C# calls the Python Area(): 13.0
print(Square(3).Describe()) # a square, a shape of area 9
async def main():
return await Survey.CountAsync(7) # a C# Task, awaited by asyncio
print(asyncio.run(main())) # 7
相关特性:
| 特性 | 作用于 | 功能 |
|---|---|---|
[PythonExport] |
类、结构体、静态类、枚举 | 生成该类型的绑定;Module 和 Name 用于选择从何处导入以及导入后的名称 |
[PythonSubclassable] |
类 | 还允许 Python 类派生自它,并重写其虚成员和抽象成员 |
[PythonInterface] |
接口 | 允许在 C# 需要该接口的地方传入任何具有相应方法的 Python 对象 |
[PythonName("...")] |
成员 | 为方法、属性或事件在 Python 中指定另一个名称(例如 GreetAll 对应 greet_all) |
[PythonIgnore] |
成员 | 对 Python 隐藏某个成员 |
方法可以带有默认参数和 params 参数,并可以用关键字实参调用;重载根据实参类型选择;属性、索引器、事件和运算符会映射到其 Python 对应物;Task 结果可以被 await。泛型类型和泛型方法,以及带有 ref 或 out 参数的成员,不会包含在生成的绑定中。
将 C# 扩展作为 Python 包发布
若要像其他包一样用 pip 安装 C# 扩展,请将程序集放入一个 Python 包中,并在该包的 __init__.py 中按路径加载它:
# geometry_tools/__init__.py
import os
import clr
clr.AddReference(os.path.join(os.path.dirname(__file__), "Geometry.dll"))
from geometry import Vector, Shape, Survey
在包的 pyproject.toml 中将 Geometry.dll 作为包数据包含进去,之后只要安装了该包,import geometry_tools 就可以在任何地方使用。