友情提示:此为高阶主题,初级码农请绕行。
2. 自定义扩展类型:教程
Python 允许编写 C 扩展模块定义可以从 Python 代码中操纵的新类型,这很像内置的
str
和
list
类型。所有扩展类型的代码都遵循一个模式,但是在您开始之前,您需要了解一些细节。这份文件是对这个主题介绍。
2.1. 基础
CPython 运行时将所有 Python 对象都视为类型
PyObject*
的变量,即所有 Python 对象的"基础类型"。
PyObject
结构体本身包含了对象的 reference count 和对象的"类型对象"。类型对象确定解释器需要调用哪些 (C) 函数,例如一个属性查询一个对象,一个方法调用,或者与另一个对象相乘。这些 C 函数被称为“类型方法”。所以,如果你想要定义新的扩展类型,需要创建新的类型对象。这类事情只能用例子解释,这里用一个最小化但完整的的模块,定义了新的类型叫做
Custom
在C扩展模块
custom
里。注解 这里展示的方法是定义
static 扩展类型的传统方法。可以适合大部分用途。C API也可以定义在堆上分配的扩展类型,使用
PyType_FromSpec()
函数,但不在本入门里讨论。
#define PY_SSIZE_T_CLEAN#include typedef struct {
PyObject_HEAD/* Type-specific fields go here. */
} CustomObject;static PyTypeObject CustomType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = "Custom objects",
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};static PyModuleDef custommodule = {
PyModuleDef_HEAD_INIT,
.m_name = "custom",
.m_doc = "Example module that creates an extension type.",
.m_size = -1,
};
PyMODINIT_FUNCPyInit_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
类的对象 object 包含了: CustomObject
结构,这会为每个 Custom
实例分配一次。
Custom
type 的行为:这是 CustomType
结构体,其定义了一堆标识和函数指针,会指向解释器里请求的操作。
- 初始化
custom
模块: PyInit_custom
函数和对应的 custommodule
结构体。
结构的第一块是
typedef struct {
PyObject_HEAD
} CustomObject;
这就是一个自定义对象所包含的。
PyObject_HEAD
是强制要求必须在每个对象结构体之前,用以定义一个类型为
PyObject
的字段叫
ob_base
,包含了一个指针指向类型对象和一个引用计数(这可以用宏
Py_REFCNT
和
Py_TYPE
来区分)。用宏来抽象,使得附加字段可以用调试构建。注解 注意在宏
PyObject_HEAD
后没有分号。意外添加分号会导致编译器提示出错。当然,对象除了在
PyObject_HEAD
存储数据外,还有额外数据;例如,如下定义了标准的Python浮点数:
typedef struct {
PyObject_HEADdouble ob_fval;
} PyFloatObject;
第二个位是类型对象的定义:
static PyTypeObject CustomType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = "Custom objects",
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
推荐使用如上C99风格的初始化,以避免列出所有的
PyTypeObject
字段,其中很多是你不需要关心的,这样也可以避免关注字段的定义顺序。在
object.h
中实际定义的
PyTypeObject
具有比如上定义更多的 字段。剩余的字段会由 C 编译器用零来填充,通常的做法是不显式地指定它们,除非你确实需要它们。我们先挑选一部分,每次一个字段:
PyVarObject_HEAD_INIT(NULL, 0)
这一行是强制的样板,用以初始化如上提到的
ob_base
字段:
.tp_name = "custom.Custom",
我们的类型的名称。这将出现在我们的对象的默认文本表示形式和某些错误消息中,例如:>>>
>>> "" + custom.Custom()Traceback (most recent call last):
File "", line 1, in TypeError: can only concatenate str (not "custom.Custom") to str
Note that the name is a dotted name that includes both the module name and the name of the type within the module. The module in this case is
custom
and the type is
Custom
, so we set the type name to
custom.Custom
. Using the real dotted import path is important to make your type compatible with the
pydoc
and
pickle
modules.
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
This is so that Python knows how much memory to allocate when creating new
Custom
instances.
tp_itemsize
is only used for variable-sized objects and should otherwise be zero. If you want your type to be subclassable from Python, and your type has the same
tp_basicsize
as its base type, you may have problems with multiple inheritance. A Python subclass of your type will have to list your type first in its
__bases__
, or else it will not be able to call your type's
__new__()
method without getting an error. You can avoid this problem by ensuring that your type has a larger value for
tp_basicsize
than its base type does. Most of the time, this will be true anyway, because either your base type will be
object
, or else you will be adding data members to your base type, and therefore increasing its size.We set the class flags to
Py_TPFLAGS_DEFAULT
.
.tp_flags = Py_TPFLAGS_DEFAULT,
All types should include this constant in their flags. It enables all of the members defined until at least Python 3.3. If you need further members, you will need to OR the corresponding flags.We provide a doc string for the type in
tp_doc
.
.tp_doc = "Custom objects",
To enable object creation, we have to provide a
tp_new
handler. This is the equivalent of the Python method
__new__()
, but has to be specified explicitly. In this case, we can just use the default implementation provided by the API function
PyType_GenericNew()
.
.tp_new = PyType_GenericNew,
Everything else in the file should be familiar, except for some code in
PyInit_custom()
:
if (PyType_Ready(&CustomType) < 0)return;
This initializes the
Custom
type, filling in a number of members to the appropriate default values, including
ob_type
that we initially set to
NULL
.
Py_INCREF(&CustomType);if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
Py_DECREF(&CustomType);
Py_DECREF(m);return NULL;
}
This adds the type to the module dictionary. This allows us to create
Custom
instances by calling the
Custom
class:>>>
>>> import custom>>> mycustom = custom.Custom()
That's it! All that remains is to build it; put the above code in a file called
custom.c
and:
from distutils.core import setup, Extension
setup(name="custom", version="1.0",
ext_modules=[Extension("custom", ["custom.c"])])
in a file called
setup.py
; then typing
$ python setup.py build
at a shell should produce a file
custom.so
in a subdirectory; move to that directory and fire up Python --- you should be able to
import custom
and play around with Custom objects.That wasn't so hard, was it?Of course, the current Custom type is pretty uninteresting. It has no data and doesn't do anything. It can't even be subclassed.注解 While this documentation showcases the standard
distutils
module for building C extensions, it is recommended in real-world use cases to use the newer and better-maintained
setuptools
library. Documentation on how to do this is out of scope for this document and can be found in the Python Packaging User's Guide.