在 Python 中使用 .NET
SteelPython 运行在 .NET 上,因此整个 .NET 类库——以及你手头的任何 .NET 程序集——都可以在同一进程中供 Python 代码使用,无需设置任何桥接:.NET 命名空间可以像 Python 包一样导入。如果你在 CPython 中用过 pythonnet,就会觉得这很熟悉,只是省去了它的 import clr 和 clr.AddReference 那套繁琐步骤。
导入 .NET 命名空间
from System import Math, Environment
from System.Collections.Generic import List, Dictionary
print(Math.Max(3, 7), Math.Round(2.567, 2)) # 7 2.57
print(Environment.ProcessorCount)
.NET 命名空间可以像 Python 包一样导入,无需事先做任何设置。Python 模块始终优先:如果 sys.path 上的某个文件夹或模块与某个命名空间同名,import 会找到 Python 的那一个。
.NET 运行时的每个命名空间都可以导入,无论其程序集是否已经加载:from System.Collections.ObjectModel import ObservableCollection 会加载定义它的程序集。
你自己的程序集可以像扩展模块一样导入。把 Acme.Pricing.dll 放在 sys.path 上的某个文件夹中——例如放在脚本旁边——然后导入它的命名空间:
from Acme.Pricing import PriceList # finds Acme.Pricing.dll on sys.path and loads it
如果程序集所在的文件夹你不想添加到 sys.path,可以用 clr 模块按路径加载它:
import clr
clr.AddReference(r"C:\Libraries\Acme.Pricing.dll")
from Acme.Pricing import PriceList
clr 还提供了几个辅助工具,下文会用到:clr.python_enum、clr.cancellable、clr.GetClrType 和 clr.reflection_enabled。为 pythonnet 编写、以 import clr 和 clr.AddReference(...) 开头的代码可以不加修改地运行。
使用 .NET 对象
通过调用类来创建对象,并像在 C# 中一样使用它们的方法、属性、字段和索引器:
from System.Text import StringBuilder
sb = StringBuilder()
sb.Append("a").Append(1).Append(2.5)
print(sb.ToString()) # a12.5
print(sb.Length) # 5
重载根据 Python 实参的类型来选择,关键字实参和参数默认值也都可以使用。.NET 对象同样符合 Python 的协议:IEnumerable 可迭代,IDisposable 可用于 with 语句,集合支持 len()、in 和索引(包括负索引),C# 中定义的运算符也可以在 Python 中使用。
from System.IO import StringWriter
with StringWriter() as w: # Dispose() is called at the end of the block
w.Write("written by .NET")
print(w.ToString())
泛型
在方括号中给出类型实参:
names = List[str]()
names.Add("Ada")
names.Add("Grace")
print(len(names), names[0], list(names)) # 2 Ada ['Ada', 'Grace']
ages = Dictionary[str, int]({"Ada": 36}) # built from a Python dict
ages["Grace"] = 85
print("Ada" in ages) # True
泛型方法会尽可能根据实参推断类型实参;否则请显式给出,例如 Enumerable.Where[int](numbers, predicate)。
out 和 ref 参数
它们会作为额外的返回值,以元组形式返回:
from System import Int32
ok, value = Int32.TryParse("42") # True 42
ok, value = Int32.TryParse("forty") # False 0
委托和事件
在 .NET 需要委托的任何地方,都可以传入 Python 函数(或 lambda),也可以用 += 订阅事件(并用 -= 取消订阅):
from System.Linq import Enumerable
evens = Enumerable.Where[int](List[int]([1, 2, 3, 4]), lambda n: n % 2 == 0)
print(list(evens)) # [2, 4]
from System.Collections.ObjectModel import ObservableCollection
items = ObservableCollection[str]()
def changed(sender, e):
print("collection changed:", e.Action, list(e.NewItems))
items.CollectionChanged += changed
items.Add("bolts") # collection changed: Add ['bolts']
枚举
.NET 枚举值的行为类似整数,可以用 |、& 和 ~ 组合:
from System.IO import FileAccess
flags = FileAccess.Read | FileAccess.Write
print(flags, flags == FileAccess.ReadWrite, int(flags)) # ReadWrite True 3
clr.python_enum(FileAccess) 会给出一个具有相同成员的 Python enum.IntFlag(或 IntEnum),供需要 Python 枚举的代码使用。
实现 .NET 接口和派生自 .NET 类
Python 类可以实现 .NET 接口或派生自 .NET 类,然后由 .NET 代码调用它的方法:
from System.Collections.Generic import IComparer
class Descending(IComparer[int]):
def Compare(self, x, y):
return y - x
numbers = List[int]([3, 1, 2])
numbers.Sort(Descending()) # .NET's sort calls the Python Compare
print(list(numbers)) # [3, 2, 1]
重写 .NET 基类的虚方法也是同样的方式,并且 super() 可以访问 .NET 的实现。
异步:等待 .NET 任务
.NET 的 Task 和 ValueTask 对象可以在 Python 协程中被 await,IAsyncEnumerable 对象可以用 async for 迭代:
import asyncio
from System.Threading.Tasks import Task
async def main():
await Task.Delay(10) # a .NET task, awaited by asyncio
return "awaited a .NET task"
print(asyncio.run(main()))
asyncio.gather 和 asyncio.wait_for 都可以与它们配合使用。若要让 asyncio 能够取消某个 .NET 操作,请用 clr.cancellable 向它传入一个取消令牌:await clr.cancellable(lambda token: client.DownloadAsync(url, token)) 会在 asyncio 任务被取消时取消该令牌。
值如何转换
值以自然的方式在 Python 和 .NET 之间传递,在内存布局允许的情况下不进行复制:
| Python | .NET |
|---|---|
None |
null |
bool、int、float |
bool、各整数类型(超出范围时引发 OverflowError)、double、float |
decimal.Decimal |
decimal,精确转换 |
str |
string、char |
datetime、date、time、timedelta |
DateTime 和 DateTimeOffset、DateOnly、TimeOnly、TimeSpan |
uuid.UUID、pathlib.Path |
Guid、FileInfo 和 DirectoryInfo |
bytes、bytearray、numpy 数组 |
byte[] 及其他数组、Memory<T> 和 ReadOnlyMemory<T>(尽可能共享而不复制) |
list、tuple、set、dict |
数组、List<T>、HashSet<T>、Dictionary<K,V> 及其接口 |
| 函数 | 委托 |
| 协程、异步生成器 | Task<T>、IAsyncEnumerable<T> |
从 .NET 返回的结果会变成相应的 Python 值——这意味着它们此后就是 Python 对象:DateTime(2026, 10, 29) 得到的是 Python 的 datetime,具有 Python 的方法(+ timedelta(days=3)),而不是 .NET 的方法(AddDays)。
.NET 数值数组通过 Python 的缓冲区协议共享内存,因此 numpy 可以就地处理它们:
from System import Array
import numpy as np
a = Array[float]([1.0, 2.0, 3.0])
np.asarray(a)[:] *= 10 # changes the .NET array itself
print(list(a)) # [10.0, 20.0, 30.0]
异常
.NET 异常到达 Python 时,会成为一个既是 .NET 类型、又是对应 Python 内置异常的异常,因此两种 except 子句都能捕获它:
try:
Int32.Parse("x")
except ValueError as e: # also catchable as System.FormatException
print(type(e).__name__) # FormatException
ArgumentException 和 FormatException 是 ValueError,KeyNotFoundException 是 KeyError,IndexOutOfRangeException 是 IndexError,InvalidOperationException 是 RuntimeError,IOException 是 OSError(FileNotFoundException 是 FileNotFoundError),NotImplementedException 是 NotImplementedError,依此类推。异常会保留其 .NET 成员:e.Message、e.StackTrace、e.InnerException。
你自己的 C# 代码
任何 .NET 类库都可以在 Python 中使用:把它的 .dll 放到 sys.path 上,然后导入它的命名空间。若要编写专为 Python 设计的库——具有自己的模块名、原生速度的绑定,以及 Python 可以继承的类——请参阅 编写扩展。若要反过来在 C# 应用程序中运行 Python,请参阅 嵌入到 C# 中。
Native AOT 构建
上述所有内容都使用了 .NET 反射,而 SteelPython 的原生编译(Native AOT)构建不包含反射。在这种构建中,import clr; clr.reflection_enabled 为 False,并且只有宿主应用程序公开的 .NET 类型才可导入。如果你不确定自己使用的是哪种构建,请检查 clr.reflection_enabled;在常规 .NET 构建中它为 True,本页的所有内容都可以正常使用。