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