基础
CPython 运行时会将所有 Python 对象都视为 PyObject* 类型的变量,这是所有 Python 对象的“基础类型”。 PyObject 结构体本身只包含对象的 reference count 和指向对象的“类型对象”的指针。 这是动作所针对的目标。 类型对象决定解释器要调用哪些 (C) 函数,例如,在对象上查找一个属性,调用一个方法,或者与另一个对象相乘等。 这些 C 函数被称为“类型方法”。
所以,如果你想要定义新的扩展类型,需要创建新的类型对象。
这种事情只能通过例子来解释,下面是一个最小但完整的模块,它在 C 扩展模块 custom 中定义了一个名为 Custom 的新类型:
备注 这里展示的方法是定义 static 扩展类型的传统方法。可以适合大部分用途。C API也可以定义在堆上分配的扩展类型,使用 PyType_FromSpec() 函数,但不在本入门里讨论。
#define PY_SSIZE_T_CLEAN
#include <;Python.h>
typedef struct {
PyObject_HEAD
/* Type-specific fields go here. */
} CustomObject;
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
static PyModuleDef custommodule = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom",
.m_doc = "Example module that creates an extension type.",
.m_size = -1,
};
PyMODINIT_FUNC
PyInit_custom(void)
{
PyObject *m;
if (PyType_Ready(&CustomType) < 0)
return NULL;
m = PyModule_Create(&custommodule);
if (m == NULL)
return NULL;
Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
Py_DECREF(&CustomType);
Py_DECREF(m);
return NULL;
}
return m;
}
这部分很容易理解,这是为了跟上一章能对接上。这个文件定义了三件事:
一个 Custom 对象 包含的东西:这是 CustomObject 结构体,它会为每个 Custom 实例分配一次。
Custom 类型 的行为:这是 CustomType 结构体,它定义了一组旗标和函数指针供解释器在收到特定操作请求时进行检查。
如何初始化 custom 模块:这是 PyInit_custom 函数及其对应的 custommodule 结构体。
结构的第一块是
typedef struct {
PyObject_HEAD
} CustomObject;
这就是一个自定义对象将会包含的内容。 PyObject_HEAD 是强制要求放在每个对象结构体之前并定义一个名为 ob_base 的 PyObject 类型的字段,其中包含一个指向类型对象和引用计数的指针(这两者可以分别使用宏 Py_TYPE 和 Py_REFCNT 来区分)。 使用宏的理由是将布局抽象出来并在 调试编译版中 中启用附加字段。
备注 注意在宏 PyObject_HEAD 后没有分号。意外添加分号会导致编译器提示出错。
当然,对象除了在 PyObject_HEAD 存储数据外,还有额外数据;例如,如下定义了标准的Python浮点数:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
第二个位是类型对象的定义:
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
备注 推荐使用如上C99风格的初始化,以避免列出所有的 PyTypeObject 字段,其中很多是你不需要关心的,这样也可以避免关注字段的定义顺序。
在 object.h 中实际定义的 PyTypeObject 具有比如上定义更多的 字段。 剩余的字段会由 C 编译器用零来填充,通常的做法是不显式地指定它们,除非你确实需要它们。
我们先挑选一部分,每次一个字段:
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
这一行是强制的样板,用以初始化如上提到的 ob_base 字段:
.tp_name = "custom.Custom",
我们的类型的名称。 这将出现在我们的对象的默认文本表示形式和某些错误消息中,例如:
>>>
"" + custom.Custom()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str
请注意此名称是一个带点号名称,它同时包括模块名称和模块中的类型名称。 本例中的模块是 custom 而类型是 Custom,因此我们将类型名称设为 custom.Custom。 使用真正的带点号的导入路径对于使你的类型与 pydoc 和 pickle 模块保持兼容是很重要的。
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
这样能让 Python 知道当创建新的 Custom 实例时需要分配多少内存。 tp_itemsize 仅用于可变大小的对象而在其他情况下都应为零。
备注 如果你希望你的类型可在 Python 中被子类化,并且你的类型和它的基类型具有相同的 tp_basicsize,那么你可能会遇到多重继承问题。 你的类型的 Python 中的子类必须在其 __bases__ 中将你的类型列表最前面,否则在调用你的类型的 __new__() 方法时将会出错。 你可以通过确保你的类型具有比其基类型最大的 tp_basicsize 值来避免这个问题。 在大多数时候,这都是可以的,因为要么你的基类型是 object,要么你将为你的基类型添加数据成员,从而增加其大小。
我们将类旗标设为 Py_TPFLAGS_DEFAULT。
.tp_flags = Py_TPFLAGS_DEFAULT,
所有类型都应当在它们的旗标中包括此常量。 该常量将启用至少在 Python 3.3 之前定义的全部成员。 如果你需要更多的成员,你将需要对相应的旗标进行 OR 运算。
我们为 tp_doc 类型提供一个文档字符串.
.tp_doc = PyDoc_STR("Custom objects"),
要启用对象创建,我们必须提供一个 tp_new 处理句柄。 这等价于 Python 方法 __new__(),但必须显式地指定。 在这种情况下,我们可以使用 API 函数 PyType_GenericNew() 所提供的默认实现。
.tp_new = PyType_GenericNew,
除了 PyInit_custom() 中的某些代码以外,文件中的其他内容应该都很容易理解:
if (PyType_Ready(&CustomType) < 0)
return;
这将初始化 Custom 类型,为一些成员填充适当的默认值,包括我们在初始时设为 NULL 的 ob_type。
Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
Py_DECREF(&CustomType);
Py_DECREF(m);
return NULL;
}
这将把类型添加到模块字典中。 这样我们就能通过调用 Custom 类来创建 Custom 实例:
>>>
import custom
mycustom = custom.Custom()
就是这样! 剩下的工作就是编译它;将上述代码放入名为 custom.c 的文件中,
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "custom"
version = "1"
名为 pyproject.toml 的文件中,并且
from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])
在名为 setup.py 的文件中;然后输入
python -m pip install .
在 shell 中应该会在子目录下产生一个文件 custom.so 并安装它;现在启动 Python --- 你应当能够执行 import custom 并尝试使用 Custom 对象。
这并不难,对吗?
当然,当前的自定义类型非常无趣。它没有数据,也不做任何事情。它甚至不能被子类化。 |