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

编写扩展

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 就可以在任何地方使用。