与 CPython 的差异
SteelPython 遵循 Python 的参考实现 CPython 3.12。除非本页另有说明,官方文档 同样适用于 SteelPython;大多数程序以及 PyPI 上的大多数包都可以不加修改地运行。
缺少的内容
| 缺少的内容 | 替代方案 |
|---|---|
tkinter、turtle、IDLE(idlelib) |
没有 Tk。请使用 Misp 作为编辑器;如需自己的窗口,可通过 clr 使用 .NET UI 库,或使用 streamlit 之类的 Web UI。 |
子解释器(_xxsubinterpreters、_xxinterpchannels) |
使用线程(会并行运行,见下文)或 multiprocessing。 |
test 包(CPython 自己的测试套件) |
不随附。 |
._pth 文件 |
设置 PYTHONPATH 或 SPYPATH,或使用虚拟环境。 |
大于 2 GiB 的单个 bytes 对象 |
更大的文件、内存映射和 memoryview 都可以正常使用;只是单个 bytes 对象(例如对 3 GB 文件执行 mmap_object[:])不能超过 2 GiB。请分片处理。 |
用户级 site 目录(pip install --user)是 SteelPython 自己的:%APPDATA%\SteelPython\Python312\site-packages,而不是 CPython 的 %APPDATA%\Python\Python312,因此一方安装的包永远不会干扰另一方。
curses 和 os.fork() 同样缺失,这与 Windows 上的 CPython 一致;multiprocessing 使用 spawn 方式启动工作进程,也与 Windows 上的 CPython 一致。
线程并行运行
SteelPython 没有全局解释器锁(GIL)。Python 线程会同时在不同的核心上运行,因此拆分到 ThreadPoolExecutor 上的 CPU 密集型工作确实会变快,就像在 CPython 3.13 及更高版本的自由线程构建上一样。
所提供的保证也与那里相同:对内置对象的单个操作(list.append、dict[key] = value、对共享迭代器调用 next() 等)是原子的,任何数据竞争都不会导致解释器崩溃或破坏其状态。复合操作——对共享变量执行 counter += 1、先检查后操作的序列——需要使用 threading.Lock,这与 Python 一直以来的要求完全相同。在 CPython 上正确的代码在 SteelPython 上也是正确的;而那些暗中依赖 GIL 来串行化复合操作的代码,现在可能会暴露出竞争问题。
对象何时被释放
CPython 会在对象的最后一个引用消失的那一刻释放它。SteelPython 的对象则由 .NET 垃圾回收器释放,回收器会不定期运行。这只对那些观察释放时机的代码有影响:
__del__方法、weakref回调和weakref.finalize会在回收器发现对象不可达时运行,或在下一次gc.collect()时运行,而不是在最后一次del时运行。它们确实会运行,只运行一次,并且在 Python 线程上运行。- 你没有关闭的文件和其他资源会在被回收时关闭(并发出通常的
ResourceWarning)。在常见情况下——文件被打开到局部变量中或作为临时对象使用,生成器或协程仅被局部变量持有——SteelPython 会在该引用消失时立即释放对象,就像 CPython 一样。在其他所有情况下,请显式关闭资源,这本来也是 Python 自己的文档所推荐的做法:使用with open(...) as f:。 sys.getrefcount()对普通对象不会报告真实的计数,gc.collect()返回的是它运行的终结器数量,而不是 CPython 的不可达对象数量。依据引用计数做决策的代码(在测试套件之外很少见)会表现得好像每个对象都是共享的。
编译文件与字节码
SteelPython 将编译后的模块缓存在 __pycache__ 中,保存为名为 module.steelpython-312.pyc 的 .pyc 文件。它们与 CPython 的 module.cpython-312.pyc 并存而不冲突,但两种实现都无法读取对方的文件。
SteelPython 不运行 CPython 字节码。dis 显示的是 CPython 针对同一源代码会生成的字节码,因此检查代码对象的工具可以正常工作;而且可以从 SteelPython 编译的函数中复制和重建代码对象(code.replace()、types.CodeType(...))。由手写字节码或由 CPython 生成的字节码组装而成的代码对象,在被调用时会引发 SystemError。代码对象的 marshal 使用 SteelPython 自己的格式。
识别解释器
| SteelPython | CPython | |
|---|---|---|
sys.implementation.name |
'steelpython' |
'cpython' |
sys.implementation.cache_tag |
'steelpython-312' |
'cpython-312' |
sys.version |
'3.12.13 (...) [SteelPython 0.1.0 on .NET 10.0, ...]' |
'3.12.13 (...) [MSC v.1944 64 bit (AMD64)]' |
platform.python_implementation() |
'CPython' |
'CPython' |
最后一行是有意为之:包通过这个问题来判断 CPython 的 C API 和行为是否可用,而在 SteelPython 上它们是可用的。要检测 SteelPython,请检查 sys.implementation.name。
调试与跟踪
pdb、breakpoint()、sys.settrace、sys.monitoring、trace、cProfile 和 faulthandler 都可以使用。有两处细粒度的细节不同:逐操作码的跟踪事件(frame.f_trace_opcodes)会被接受但从不报告;sys.monitoring 按每行而不是按每条字节码指令报告一次指令事件。调试器和覆盖率工具所使用的行级调试与 CPython 相同。
交互式提示符
提示符和 input() 使用 SteelPython 自己的行编辑器,支持 GNU readline 的 Emacs 风格按键(历史记录、Ctrl+R 搜索、剪切和粘贴、Tab 补全、~/.inputrc 文件)。它没有 vi 模式。
性能
预计大多数纯 Python 程序的运行时间会比在 CPython 3.12 上稍长:在典型的应用程序工作负载下,大约是其 1.5 到 2.5 倍。有些类型的代码比在 CPython 上运行得更快——尤其是紧凑的数值循环——而另一些则更慢,因此请对你关心的程序进行测量。
- 启动很快:解释器在不到一秒的时间内启动,与 CPython 相当。
- 在编译包内部完成的工作(对大数组执行的 numpy 运算、pandas 分组聚合、在 GPU 上运行的 PyTorch 模型)以该包自身的速度运行,因为它们是同样的编译代码。
- 大量对编译包的微小调用(对单个 numpy 标量执行数百万次运算、用 mypyc 编译的程序)每次调用的开销都比在 CPython 上更大。请把更大块的工作交给包来完成:进行向量化。
- 线程可以让 CPU 密集型 Python 代码用上你所有的核心,这是 CPython 3.12 做不到的。一个在单核上较慢的程序,在八核上可能会快得多。
- 一段代码的首次运行比同一进程中后续的运行慢,因为 .NET 需要编译它;长时间运行的程序和服务器在预热后会达到全速。