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