CPython C 扩展模块隔离指南:per-module state 与 heap types 的完整实践
本篇指南基于 CPython 官方文档 Doc/howto/isolating-extensions.rst 展开,主题是如何把 C 扩展模块从“进程级全局状态”改造为“模块级状态(per-module state)”,使其在 Python 被当作库嵌入(多次初始化/反初始化、子解释器并行)的场景下安全运行。读完本文,你将掌握:如何用 Modules/xxlimited.c 这样的参考实现来设计模块状态、如何通过 m_size 请求模块私有存储、如何把静态类型(static type)改造为堆类型(heap type),以及模块函数、类方法、slot 方法三种场景下访问模块状态的完整 API 调用链,并能在源码级别(Include/moduleobject.h、Objects/moduleobject.c、Objects/typeobject.c)验证这些机制。
适用读者与背景:为什么进程级状态是隐患
谁来读这份指南
本指南面向 C-API 扩展的维护者,目标是让你的扩展在“Python 自身被用作库”的应用中更安全。典型场景是宿主程序在一个进程中反复执行 Py_InitializeEx/Py_FinalizeEx 循环,或用 Py_NewInterpreter/Py_EndInterpreter 管理并行的子解释器。库代码通常不应假设宿主存在一个进程级的“主解释器”,但历史上大量扩展模块(甚至部分标准库模块)因为 C static 变量用起来实在太方便,而把本应属于某个解释器的数据放在了进程级全局状态里。
核心问题:数据被多个解释器共享
当一个模块同时加载进同一进程中的多个解释器时,进程级 static 状态会被共享,极易引入导致崩溃的边界情况。更麻烦的是 per-interpreter 状态本身难以实现——扩展作者开发时往往根本没考虑多解释器,而且测试这种行为的成本很高。
CPython 的应对方向是让 C-API 向更细粒度的 per-module state 演进:C 层数据挂到“模块对象”上。每个解释器创建自己的模块对象,数据天然隔离;为了测试隔离性,甚至可以在同一解释器中加载同一扩展的多个模块对象。per-module state 还提供了直观的生命周期与资源归属模型:模块对象创建时初始化、释放时清理。就像任何其他 PyObject * 一样,不需要(也无需忘记)“解释器关闭钩子”。
需要注意的是,per-process、per-interpreter、per-thread 或 per-task 状态仍有其用途。以 per-module 为默认之后,这些需求仍然可以实现,但应视为例外情况,需要额外的设计与测试(本文不展开)。
隔离的模块对象:同一共享库可以产生多个模块对象
开发扩展模块时牢记一点:同一个共享库可以创建出多个模块对象。文档给出的示例如下:
>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii # create a new module object
>>> old_binascii == binascii
False
经验法则是:两个模块应当完全独立。模块私有的对象和状态都应封装在模块对象内部,不与其他模块对象共享,并在模块对象被回收时清理。由于只是经验法则,例外是允许的(见下文“管理全局状态”),但例外需要更多对边界情况的思考。
反直觉的边界情况:异常类不共享
隔离模块会带来一些出乎意料的边界情况。最典型的是:每个模块对象通常不会与其他同名模块共享其类和异常。接上例,old_binascii.Error 与 binascii.Error 是两个不同对象,下面的异常捕获不到:
>>> old_binascii.Error == binascii.Error
False
>>> try:
... old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
... print('boo')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found
这是预期行为——纯 Python 模块也是同样的表现,属于 Python 工作机制的一部分。目标是让扩展在 C 层面安全,而不是让 hack 行为直观;手动改 sys.modules 就属于 hack。
管理全局状态:模块是“访问者”而非“所有者”
有些模块的状态不属于模块本身,而是属于整个进程。典型例子:
readline模块管理的是 那一个 终端;- 运行在电路板上的模块要控制 那块板上的 LED。
此时 Python 模块应该提供对全局状态的访问,而不是拥有它。如果可能,应写成允许多个副本独立访问该状态(与其他语言的库并存);做不到时,考虑显式加锁。
如果确有必要使用进程级全局状态,最简单的避免多解释器问题的方式是:显式禁止模块在一个进程中加载超过一次。
Opt-Out:用进程级标志限制每进程一个模块对象
PyModuleDef.m_size >= 0 表示模块正确支持多解释器;如果你的模块还没做到,可以用下面的方式让它每进程只能加载一次:
// A process-wide flag
static int loaded = 0;
// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};
static int
exec_module(PyObject* module)
{
PyMutex_Lock(&modinit_mutex);
if (loaded) {
PyMutex_Unlock(&modinit_mutex);
PyErr_SetString(PyExc_ImportError,
"cannot load module more than once per process");
return -1;
}
loaded = 1;
PyMutex_Unlock(&modinit_mutex);
// ... rest of initialization
}
从 Include/moduleobject.h 可以看到 m_size 在 struct PyModuleDef 中的位置(该结构还包含 m_methods、m_slots 以及 m_traverse、m_clear、m_free 三个状态钩子),而 PyModuleDef_Base 中的注释明确写着 m_init 只在 legacy 模块且 m_size >= 0 时由 _PyImport_LoadDynamicModuleWithSpec() 等路径设置——也就是说,m_size 的符号位是解释器判断“是否支持多次初始化”的开关。
如果模块的 PyModuleDef.m_clear 能够为将来重新初始化做好准备,就应当在那里清除 loaded 标志。此时模块虽然不支持多个实例并发存在,但支持在 Python 运行时关闭(Py_FinalizeEx)后重新初始化(Py_Initialize)再加载。
管理模块状态:m_size 与状态钩子
用 m_size 请求模块私有存储
使用 per-module state 的前提是采用多阶段(multi-phase)扩展模块初始化。这同时向解释器宣告你的模块正确支持多解释器。
把 PyModuleDef.m_size 设为正数,即请求相应字节的模块本地存储。惯例是把它设为某个模块专用 struct 的大小,用来保存模块的全部 C 层状态,尤其是:类指针(含异常类,但不含 static types)和 C 代码运行所需的设置(例如 csv 的 field_size_limit)。
参考实现 Modules/xxlimited.c 的状态结构长这样(该文件头部注释也自述它是 Limited API 模板,建议改名为你的模块名后使用):
// Module state
typedef struct {
PyTypeObject *Xxo_Type; // Xxo class
PyObject *Error_Type; // Error class
} xx_state;
在 3.15 新版 API 下,该模块用 slots 数组声明状态大小与钩子(见 Modules/xxlimited.c,PySlot_SIZE(Py_mod_state_size, sizeof(xx_state))、PySlot_FUNC(Py_mod_state_traverse, xx_traverse) 等);而在更早的写法中,对应的 Modules/xxlimited_35.c 则直接使用 struct PyModuleDef 字面量,其中 m_size 为 0(无状态模块,钩子均为 NULL):
static struct PyModuleDef xxmodule = {
PyModuleDef_HEAD_INIT,
"xxlimited_35",
module_doc,
0, // m_size
xx_methods,
xx_slots,
NULL, // m_traverse
NULL, // m_clear
NULL // m_free
};
存进 dict 的风险与适用场景
另一个选择是把状态放进模块的 __dict__,但前提是你能保证用户从 Python 侧改坏 __dict__ 时不崩溃——这通常意味着 C 层的错误处理与类型检查,容易写错且难以充分测试。如果 C 代码不需要直接访问模块状态,那么只存 __dict__ 是合理选择。
状态里含 PyObject 指针时必须实现三个钩子
若模块状态包含 PyObject 指针,模块对象必须持有这些对象的引用,并实现模块级钩子 m_traverse、m_clear、m_free。它们的角色与类的 tp_traverse、tp_clear、tp_free 相同。添加这些钩子需要额外工作、代码变长——这就是“模块可干净卸载”的代价。
Modules/xxlimited.c 给出了完整的三件套写法:
static int
xx_traverse(PyObject *module, visitproc visit, void *arg)
{
xx_state *state = PyModule_GetState_DuringGC(module);
if (state == NULL) {
return 0;
}
Py_VISIT(state->Xxo_Type);
Py_VISIT(state->Error_Type);
return 0;
}
static int
xx_clear(PyObject *module)
{
xx_state *state = PyModule_GetState(module);
if (state == NULL) {
return 0;
}
Py_CLEAR(state->Xxo_Type);
Py_CLEAR(state->Error_Type);
return 0;
}
static void
xx_free(void *module)
{
// allow xx_modexec to omit calling xx_clear on error
(void)xx_clear((PyObject *)module);
xx_state *state = PyModule_GetState(module);
if (state == NULL) {
return;
}
}
注意两个细节:m_traverse 中要用 PyModule_GetState_DuringGC 而非普通 PyModule_GetState(后者不允许在 GC 遍历期间调用),m_free 中先兜底执行一次 xx_clear,保证初始化中途失败也不会泄漏引用。
在源码层面,PyModule_GetState 的实现非常薄(见 Objects/moduleobject.c):校验 PyModule_Check(m) 后直接返回 _PyModule_GetState(m)。这也印证了文档的提醒:当 m_size 为 0(无模块状态)时,PyModule_GetState 可能在未设置异常的情况下返回 NULL——在你自己的模块里 m_size 由你控制,因此可以轻松规避。
Heap Types:从静态类型到堆类型
静态类型为什么不够
传统上 C 代码中定义的类型是静态的:static PyTypeObject 结构体直接写死在代码里,用 PyType_Ready() 初始化。这种类型必然被整个进程共享;在不同模块对象之间共享它们,就必须留意它们拥有或访问的任何状态。为了限制问题范围,静态类型在 Python 层面是不可变的——例如你无法执行 str.myattribute = 123。
实现细节上(文档以 impl-detail 标注):共享真正不可变的对象本身没问题,只要它们不暴露可变对象。但 CPython 中每个 Python 对象都有一个可变的实现细节——引用计数。引用计数的修改受 GIL 保护,因此跨解释器共享任何 Python 对象,都隐式依赖 CPython 当前进程级 GIL 的实现。
由于静态类型不可变、进程全局,它们无法访问“自己那个”模块状态。只要某个方法需要访问模块状态,该类型就必须改造为堆分配类型(heap type)——它更接近 Python class 语句创建的类。对新模块,默认使用 heap types 是好习惯。
静态类型转堆类型:两个必须注意的差异
静态类型可以转换为堆类型,但注意:heap type API 并非为“无损”转换而设计——即无法保证造出一个与给定静态类型行为完全一致的类型。改用新 API 重写类定义时,很容易无意中改变一些细节(如可 pickle 性、继承的 slots),务必测试你在意的每个细节。特别留意(非穷尽)以下两点:
- 与静态类型不同,heap type 对象默认可变。用
Py_TPFLAGS_IMMUTABLETYPE标志禁止可变性。 - heap types 默认继承
tp_new,因此可能可以从 Python 代码实例化。用Py_TPFLAGS_DISALLOW_INSTANTIATION标志阻止这一点。
用 PyType_Spec 与 PyType_FromModuleAndSpec 定义堆类型
堆类型通过填充 PyType_Spec 结构(类的“蓝图”)并调用 PyType_FromModuleAndSpec 来创建。注意:PyType_FromSpec 等函数也能创建 heap type,但 PyType_FromModuleAndSpec 会把模块与类关联起来,从而允许从方法中访问模块状态。
类对象应当同时存放在两处:模块状态中(供 C 代码安全访问)和模块的 __dict__ 中(供 Python 代码访问)。Modules/xxlimited.c 的初始化代码示范了这一点:
state->Error_Type = PyErr_NewException("xxlimited.Error", NULL, NULL);
if (state->Error_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, (PyTypeObject*)state->Error_Type) < 0) {
return -1;
}
state->Xxo_Type = (PyTypeObject*)PyType_FromModuleAndSpec(
m, &Xxo_Type_spec, NULL);
if (state->Xxo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, state->Xxo_Type) < 0) {
return -1;
}
PyModule_AddType 会把类同时放进模块 __dict__ 并持有引用——类既保存在 xx_state 里,也暴露在模块命名空间中。该文件还展示了另一种用法:Str_Type 只需从 Python 访问,因此只加入模块字典(基类指定为 PyUnicode_Type)。在 C 侧,由于 Xxo 类型是动态分配的、一个进程中可能存在多份(不同子解释器或重复加载),文件里通过 Xxo_state_from_type() 基于静态不变的 Xxo_Type_spec 做基类搜索来回溯模块状态,而非保存任何“全局”类型指针——这正是多模块对象场景下的正确姿势。
GC 协议:heap type 实例必须能参与回收
heap type 的实例持有对其类型的引用(保证类型晚于实例存活),但这可能形成引用环,需要垃圾回收器来打破。为避免内存泄漏,heap type 实例必须实现 GC 协议:
- 带上
Py_TPFLAGS_HAVE_GC标志; - 用
Py_tp_traverse定义 traverse 函数,访问其类型(例如Py_VISIT(Py_TYPE(self)))。
heap type 定义 API 是自然生长出来的,目前用起来略别扭。以下逐条给出常见问题的官方建议。
tp_traverse 在 Python 3.8 及更早版本的处理
“从 tp_traverse 访问类型”这一要求是 Python 3.9 加入的。若还要支持 3.8 及更早版本,traverse 函数不能访问类型,必须写得更复杂:
static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
return 0;
}
可惜 Py_Version 是 Python 3.11 才有的宏。替代方案:
- 不使用 Stable ABI 时用
PY_VERSION_HEX; - 或通过
PySys_GetObject和PyArg_ParseTuple读取sys.version_info。
委托 tp_traverse 时避免重复访问
若 traverse 函数委托给基类(或其他类型)的 tp_traverse,确保 Py_TYPE(self) 只被访问一次。注意只有 heap type 才被期望在 tp_traverse 中访问类型。例如若你的 traverse 包含:
base->tp_traverse(self, visit, arg)
而 base 可能是 static type,则还应写:
if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
// a heap type's tp_traverse already visited Py_TYPE(self)
} else {
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
}
无需在 tp_new 和 tp_clear 中处理类型的引用计数。
自定义 tp_dealloc 的两件事
若类型有自定义 tp_dealloc,必须:
- 在使任何字段失效之前调用
PyObject_GC_UnTrack; - 递减类型的引用计数。
为了让类型在 tp_free 被调用期间保持有效,类型引用计数必须在实例被释放之后再递减。例如:
static void my_dealloc(PyObject *self)
{
PyObject_GC_UnTrack(self);
...
PyTypeObject *type = Py_TYPE(self);
type->tp_free(self);
Py_DECREF(type);
}
默认 tp_dealloc 已经做了这件事,所以如果你的类型没有覆写 tp_dealloc,就不需要自己加。
不要覆写 tp_free
heap type 的 tp_free 槽必须是 PyObject_GC_Del。这是默认值,不要覆盖。
避免 PyObject_New:GC 对象要用 GC 感知的分配函数
若你使用 PyObject_New 或 PyObject_NewVar:
- 在可能的情况下,取类型的
tp_alloc槽并调用它。即把TYPE *o = PyObject_New(TYPE, typeobj)替换为:
TYPE *o = typeobj->tp_alloc(typeobj, 0);
o = PyObject_NewVar(TYPE, typeobj, size) 同理替换,只是用 size 代替 0。
- 若上面做不到(例如在自定义
tp_alloc内部),改调PyObject_GC_New或PyObject_GC_NewVar:
TYPE *o = PyObject_GC_New(TYPE, typeobj);
TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
三种场景下访问模块状态
场景一:模块级函数
模块级函数访问状态很直接:函数拿到模块对象作为第一个参数,用 PyModule_GetState 提取状态:
static PyObject *
func(PyObject *module, PyObject *args)
{
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
// ... rest of logic
}
再次注意:m_size 为 0 时 PyModule_GetState 可能在未设置异常的情况下返回 NULL;你自己的模块掌控 m_size,因此容易规避。
场景二:类方法(3.9+ 的 PyCMethod 协议)
从类方法访问模块状态稍复杂,但得益于 Python 3.9 引入的 API 可以实现。关键第一步是拿到定义类(defining class),再从它获取模块状态。最大的障碍是如何拿到“方法定义于哪个类”。
不要把定义类与 Py_TYPE(self) 混淆:当方法在类型的子类上被调用时,Py_TYPE(self) 指向那个子类,它可能定义在你的模块之外。文档用这段 Python 代码解释概念——即使 type(self) == Sub,Base.get_defining_class 仍返回 Base:
class Base:
def get_type_of_self(self):
return type(self)
def get_defining_class(self):
return __class__
class Sub(Base):
pass
要拿到定义类,方法必须采用 METH_METHOD | METH_FASTCALL | METH_KEYWORDS 调用约定和对应的 PyCMethod 签名:
PyObject *PyCMethod(
PyObject *self, // object the method was called on
PyTypeObject *defining_class, // defining class
PyObject *const *args, // C array of arguments
Py_ssize_t nargs, // length of "args"
PyObject *kwnames) // NULL, or dict of keyword arguments
拿到定义类后调用 PyType_GetModuleState 即可:
static PyObject *
example_method(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
Py_ssize_t nargs,
PyObject *kwnames)
{
my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
if (state == NULL) {
return NULL;
}
... // rest of logic
}
PyDoc_STRVAR(example_method_doc, "...");
static PyMethodDef my_methods[] = {
{"example_method",
(PyCFunction)(void(*)(void))example_method,
METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
example_method_doc}
{NULL},
}
而 PyType_GetModuleState 本身就是 PyType_GetModule + PyModule_GetState 的组合,省去样板错误处理:
my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
return NULL;
}
从源码看(Objects/typeobject.c),PyType_GetModuleState 内部就是先取类型关联的模块、再调 _PyModule_GetState,与文档描述一致;Lib/test/test_stable_abi_ctypes.py 中也列有 PyType_GetModuleState、PyType_GetModuleByDef 等符号,佐证这些函数属于受限 API(limited API)的测试覆盖范围。
场景三:slot 方法与 getter/setter(3.11+)
该能力为 Python 3.11 新增。若使用 limited API,必须把
Py_LIMITED_API升到0x030b0000,将失去与更早版本的 ABI 兼容。
slot 方法(特殊方法的快速 C 实现,如 nb_add 对应 __add__、tp_new 对应初始化)的 API 非常简单,不像 PyCMethod 那样能传入定义类;用 PyGetSetDef 定义的 getter/setter 亦然。
这种情况用 PyType_GetModuleByDef,把模块定义(PyModuleDef)作为参数传入;拿到模块后再调 PyModule_GetState:
PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
PyType_GetModuleByDef 的工作方式是搜索 MRO(即所有超类),找到第一个带有对应模块的超类。实现见 Objects/typeobject.c——找不到时会抛出 "No superclass of '%s' has the given module" 错误。文档还特别指出:在极特殊的场景(跨多个由同一定义创建的模块的继承链)下,它可能不返回真正定义类的模块,但总会返回一个定义相同的模块,从而保证兼容的 C 内存布局。
模块状态的生命周期与未决问题
生命周期:状态指针的存活依赖模块引用
模块对象被垃圾回收时,其模块状态随之释放。因此对于每一个指向(状态的一部分的)指针,你必须持有模块对象的引用。
通常这不是问题:用 PyType_FromModuleAndSpec 创建的类型(以及它们的实例)都持有对模块的引用。但当你从别处引用模块状态时(例如外部库的回调)必须格外小心引用计数。
未决问题:per-class scope 与无损转换
文档指出,围绕 per-module state 与 heap types 仍有若干未决问题(相关讨论建议在 C-API 社区论坛进行):
- Per-class scope:截至 Python 3.11,无法在不依赖 CPython 实现细节(未来可能变化——也许 ironically 正是为了允许 per-class scope 的正解)的前提下,把状态挂到单独的类型上。
- 无损转换 heap types:heap type API 并非为静态类型的“无损”转换而设计——无法保证造出一个与给定静态类型行为完全一致的类型。
小结:一张可执行清单
结合 Doc/howto/isolating-extensions.rst 与仓库实现,改造一个 C 扩展模块的推荐路径是:
- 确认初始化方式:采用 multi-phase 初始化,把
PyModuleDef.m_size设为模块状态struct的大小(参考 Include/moduleobject.h 中struct PyModuleDef的字段定义);无法做到时,用loaded标志 + 互斥量做每进程一次的 opt-out。 - 状态入模块,类与设置入状态:类指针(含异常类)与 C 侧需要的配置全部放进模块状态;C 代码不需要读的状态只放
__dict__即可。 - 状态含 PyObject 指针时:实现
m_traverse(GC 期间用PyModule_GetState_DuringGC)、m_clear、m_free,参考 Modules/xxlimited.c 的完整三件套。 - 类型全部改为 heap type:
PyType_Spec+PyType_FromModuleAndSpec,类同时存入状态与模块字典;按需加Py_TPFLAGS_IMMUTABLETYPE、Py_TPFLAGS_DISALLOW_INSTANTIATION,遵循 GC 协议(Py_TPFLAGS_HAVE_GC+ 访问Py_TYPE(self)的 traverse;tp_free保持PyObject_GC_Del;分配用tp_alloc/PyObject_GC_New)。 - 按场景取状态:模块函数用
PyModule_GetState;类方法用PyCMethod的defining_class+PyType_GetModuleState(3.9+);slot 方法与 getter/setter 用PyType_GetModuleByDef(3.11+)。 - 管理好引用:任何持有状态指针的长寿命对象(回调等)必须持有模块引用。
这样改造后,你的扩展模块即使在同一进程中被加载为多个模块对象、运行于多个子解释器,其行为也能与纯 Python 模块一样边界清晰、可测试、可卸载。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00