Python C语言API系列教程(二、PyObject是什么)
在上一篇文章中,我们讲了Python的C语言API的总体概念,并且实现了一个非常简单的Python模块,但是还遗留了一些问题。在这一章中,我们会讲解PyObject这个对象及其相关的方法,并引入内存管理相关内容。同样,我们也会继续完善datetimecpy项目(repo戳这里),难度也会变大哦。
要点讲解
PyObject介绍
在学习Python的时候你肯定会听过一句话——“在Python的世界中,一切皆对象”,然而有不少读者认为这只是一门面向对象语言的说辞,因为其他面向对象语言(比如Java,Object-C和C#等)都是这么描述自己的语言的。但是Python并不止步于此,除了语法层面的事物(比如list
、int
和generator
等一切)是对象以外,其解释器内部结构(比如帧栈、字节码和异常堆栈等)也是对象,而这个对象在Python层面是object
类,在C层面就是PyObject
!
在C语言API中PyObject是一个结构体,其包含两个成员ob_refcnt
和ob_type
。前者记录了每个对象的引用次数(本章后续会讲到),而后者是一个指向当前对象描述(PyTypeObject
)的指针(暂且称之为对象描述)。也就是说,所有对象都是PyObject
对象,区别只是在于描述的指针不同罢了。
PyTypeObject和相关插槽
PyTypeObject
是对象描述,同样也是一个对象。在Python的C语言编程中,每一个对象几乎都有这样一个对象描述与其对应。
在使用过程中,PyTypeObject
可以静态初始化,也可以动态初始化。其成员变量即为Python对象的种种属性(部分是魔法函数的平替),比如tp_name
表示Python对象的名称、tp_str
表示Python的__str__
函数、tp_call
表示Python的__call__
函数、tp_init
表示Python的__init__
函数。这些属性可以按需初始化,只要在集成进Python解释器之前确保调用Py_READY
宏可对未初始化的成员进行默认值初始化。因此,这些属性又可以被称为“插槽”。
引用计数
Python是一门有垃圾回收机制(GC)的语言,和Java的GC原理不同,其采用简单的引用计数作为内存跟踪方法。
目前主流的垃圾回收机制的内存跟踪方法有两种,一是引用计数,二是可达性分析。引用计数的思想是当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。Python就是采用这种方式作为内存跟踪方法。
之前讲过PyObject
有一个字段ob_refcnt
记录了引用次数,但用户不需要直接操作这个变量,而是通过API提供的宏。Py_INCREF
宏可以简单的使对象的引用次数加一,而Py_DECREF
宏可以使对象的引用次数减一,并在引用次数达到0时销毁对象。
由于引用计数的存在,CPython还引入了引用所有权的概念,即规定了谁来负责操作对象的引用次数。有两种引用所有权:
- 强引用(strong reference):这个对象的所有权归用户,即用户需要调用
Py_INCREF
或Py_DECREF
来控制其引用次数。 - 借引用(borrowed reference):即“借”来的引用,用户不需要操作其引用次数,API帮用户实现了引用次数的操作。用户只是借来用一下。比如
PyList_getItem
、PyTuple_getItem
等API方法。
此外,还引入了一个新的行为,叫偷引用(steal reference),意思是将原本由API负责的对象转为由用户负责,类似“偷”的意思。
一些常用的基本方法
除了一些基本概念外,这里再补充一些常用的API方法——
-
PyArg_ParseTupleAndKeywords
:该方法将传入函数的参数(Python对象)转化为C语言类型。方法的参数列表长度不固定,包括传入函数的参数(Python对象)、参数格式等,方法返回一个int值表示是否成功,0表示成功否则失败。用户对其构造的对象是borrowed reference。 -
PyUnicode_FromFormat
:该方法将C语言类型对象转变为Python的str
对象。方法的参数列表长度不固定,包括待转变的C语言类型变量、参数格式等,方法返回一个PyUnicode
对象。用户拥有这个PyUnicode
对象的所有权。 -
PyObject_New
:该方法新建一个Python对象。方法参数为PyObject
和PyTypeObject*
,表示需要新建的Python对象类型,其返回值为新建的Python对象。用户拥有这个Python对象的所有权。
操作实践
现在我们继续完善我们的datetimecpy项目——
同样,本系列文章代码的repo在这里,本章代码在Ch-2分支上。
与上一篇一样,我们同样需要先用Python打一个草稿,它需要实现以下功能:
>>> import datetimecpy
>>> date1 = datetimecpy.date() # __new__函数和__init__ 函数,默认传参
>>> date1 # __repr__函数
datetimecpy.date(1683644224)
>>> date2 = datetimecpy.date(timestamp=1683644000) # __init__函数,timestamp传参
>>> date2
datetimecpy.date(1683644000)
>>> date1 < date2 # __lt__函数、__le__函数,__gt__函数等
False
>>> datetimecpy.date.today() # today函数(类函数)
datetimecpy.date(1683644345)
>>> date1.totimestamp() # totimestamp函数(成员函数)
1683644224
首先引入头文件,由于需要刻画方法描述,所以在上一张基础上多引入一个头文件——
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"
然后就是我们本期的主角,date
对象,是一个PyObject
结构体——
typedef struct {
PyObject_HEAD
long long timestamp;
} Date;
在这个结构体中,首先用宏PyObject_HEAD
来表明这是一个PyObject
,然后在里面定义一个成员timestamp
,它是一个long long
类型的C变量,用于存放当前时间戳。
然后实现该对象三个基本函数,tp_dealloc
函数、tp_new
函数和tp_init
函数,其中tp_dealloc
函数是必选的析构函数,另外两个函数分别对应Python的__new__
函数和__init__
函数,是可选的。
void Date_dealloc(PyObject* self) {
Py_TYPE(self)->tp_free(self);
}
tp_dealloc
函数只做了一件事,通过Py_TYPE
宏获取当前实例的PyTypeObject
,然后调用它的tp_free
方法将其释放。那么PyTypeObject
怎么定义呢?
static PyTypeObject Date_type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetimecpy.date",
.tp_basicsize = sizeof(Date),
.tp_dealloc = (destructor) Date_dealloc,
.tp_free = PyObject_Del
};
和之前的PyModuleDef
一样,我们采用静态初始化的方法定义一个PyTypeObject
对象,并同样用一个宏PyVarObject_HEAD_INIT(NULL, 0)
来初始化它。除此以外,还需要规定Python对象的名称tp_name
、基本大小tp_basicsize
、刚刚定义的析构函数tp_dealloc
以及析构函数中用到的释放函数tp_free
。释放函数是用了API提供的PyObject_Del
方法。
这里采用C99的指定初始化(designated initialization),目前已经被主流C编译器支持了。
然后我们再实现另外两个函数,tp_new
函数和tp_init
函数——
PyObject* Date_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
Date* self;
self = (Date*)type->tp_alloc(type, 0);
if (self) {
time_t now = time(NULL);
self->timestamp = (long long)now;
}
return (PyObject*)self;
}
tp_new
函数的签名和其他函数稍许不同,其第一个参数是PyTypeObject
。我们同样利用它的tp_alloc
函数初始化它的内存空间。tp_alloc
函数可以不需要在PyTypeObject
中定义,因为后续的Py_READY
宏可以帮助我们自动填写缺失的成员。内存分配好以后将timestamp
成员变量初始化为当前时间戳并返回。
int Date_init(PyObject* self, PyObject* args, PyObject* kwds) {
static char* kwlist[] = {"timestamp", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|L", kwlist, &((Date*)self)->timestamp)) {
return -1;
}
return 0;
}
tp_init
函数是__init__
函数的平替,参数是三个PyObject*
。self
就是Python的self
代表当前对象。args
代表参数列表,相当于Python的*args
。kwds
代表参数键值对,相当于Python的**kwargs
。初始化函数本质通过PyArg_ParseTupleAndKeywords
函数实现一次赋值。前面已经讲过这个函数的用法,具体到这个场景就是:将__init__
函数的参数以列表或键值对的形式转换成C语言的long long
类型并赋值给成员变量timestamp
。需要注意的是这里有一个参数格式|L
,代表转换成一个可选的long long
类型。除此以外还有多种表示,比如i
代表int
、s
代表const char*
和O
代表PyObject*
等等,具体可以参考官方文档。
同样,我们也需要将实现好的tp_new
函数和tp_init
函数放入描述内——
static PyTypeObject Date_type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetimecpy.date",
.tp_basicsize = sizeof(Date),
.tp_dealloc = (destructor) Date_dealloc,
.tp_init = Date_init,
.tp_new = Date_new,
.tp_free = PyObject_Del
};
接下来,我们实现另外两个魔术函数__repr__
函数和richcompare函数(包括__lt__
、__gt__
和__ne__
等)——
PyObject* Date_repr(PyObject* self) {
return PyUnicode_FromFormat("%s(%lld)", Py_TYPE(self)->tp_name, ((Date*)self)->timestamp); }
PyObject* Date_richcompare(PyObject* self, PyObject* other, int op) {
long long diff = ((Date*)self)->timestamp - ((Date*)other)->timestamp;
Py_RETURN_RICHCOMPARE(diff, 0, op);
}
__repr__
直接调用了PyUnicode_FromFormat
方法将对象以Pythonstr
的形式展现出来。这个方法前文已经讲过,类似于sprintf
函数将参数格式化返回,这里参数格式是%s(%lld)
,其中%s
代表字符串%lld
代表长整型,与给定对象相结合就是这种效果datetimecpy.date(1683644224)
。这个方法是返回一个用户管理的引用对象,但是由于直接返回了,所以我们不需要对其Py_INCREF
或者Py_DECREF
。
richcompare方法参数依次是比较对象、被比较对象以及比较符号,因此它对应着以下几个魔法函数:
__lt__
,小于__le__
,小于等于__eq__
, 等于__ne__
,不等于__gt__
,大于__ge__
。大于等于
类似Python的实现方式,该方法直接通过两个数值相减并调用宏实现的。同样,我们需要把上面两个函数放到描述内——
static PyTypeObject Date_type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetimecpy.date",
.tp_basicsize = sizeof(Date),
.tp_dealloc = (destructor) Date_dealloc,
.tp_repr = (reprfunc) Date_repr,
.tp_init = Date_init,
.tp_new = Date_new,
.tp_richcompare = (richcmpfunc)Date_richcompare,
.tp_free = PyObject_Del
};
然后是两个函数,一个是类函数today
,它用来返回当前日期的时间戳;另一个是成员函数totimestamp
,它用来返回给定日期的时间戳。现在来看一下具体实现——
PyObject* Date_today(PyObject* self, PyObject* Py_UNUSED(args)) {
Date* retval = PyObject_New(Date, &Date_type);
if (!retval) {
return NULL;
}
time_t now = time(NULL);
retval->timestamp = (long long)now;
return retval;
}
同样非常直白,直接调用C语言相关API,并返回。这里调用了一次PyObject_New
新建了一个对象,同样由于直接返回的,所以不需要我们对其引用次数操作。
PyObject* Date_totimestamp(PyObject* self, PyObject* Py_UNUSED(args)) {
long long _timestamp = ((Date*)self)->timestamp;
return PyLong_FromLongLong(_timestamp);
}
这个函数的实现过程也无需多言。但是关键来了,怎么区分是类函数还是成员函数呢?我们通过方法描述实现——
static PyMethodDef date_methods[] = {
{"today", Date_today, METH_NOARGS | METH_CLASS, PyDoc_STR("today\n-\n\n Get the current date.")},
{"totimestamp", Date_totimestamp, METH_NOARGS, PyDoc_STR("totimetamp\n-\n\n Get the current timestamp.")},
{NULL, NULL}
};
将刚刚定义好的函数放入PyMethodDef
的列表内,列表元素按照函数名,函数指针,函数标志和文档说明排列,并在最后加上{NULL, NULL}
哨兵。函数标志有很多种,用来描述不同的性质,比如METH_NOARGS
代表函数不传参而METH_CLASS
代表是一个类方法。具体说明可以参考官方文档。
最后,把这个描述放在PyTypeObject
内,最终的效果是这样的——
static PyTypeObject Date_type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetimecpy.date",
.tp_basicsize = sizeof(Date),
.tp_dealloc = (destructor) Date_dealloc,
.tp_repr = (reprfunc) Date_repr,
.tp_str = (reprfunc) Date_repr,
.tp_getattro = (getattrofunc) PyObject_GenericGetAttr,
.tp_setattro = (setattrofunc) PyObject_GenericSetAttr,
.tp_flags = (Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE),
.tp_doc = doc_date,
.tp_init = Date_init,
.tp_new = Date_new,
.tp_richcompare = (richcmpfunc)Date_richcompare,
.tp_methods = date_methods,
.tp_free = PyObject_Del
};
这里面我又补充了部分插槽,比如tp_getattro
、tp_setattro
等。
到目前为止,我们已经完成了datetimecpy
的date
对象的设计。要能够在程序中使用我们还必须在import的过程中成功加载这个对象。回到第一章讲过的模块初始化函数,增加以下内容——
PyMODINIT_FUNC PyInit_datetimecpy() {
if (PyType_Ready(&Date_type) < 0) {
return NULL;
}
PyObject* m = PyModule_Create(&datetimecpy_def);
if (!m)
{
return NULL;
}
PyModule_AddStringConstant(m, "__author__", "littlebutt");
PyModule_AddStringConstant(m, "__version__", "1.0.0");
Py_INCREF(&Date_type);
if (PyModule_AddObject(m, "date", (PyObject*)&Date_type) < 0)
{
Py_DECREF(&Date_type);
Py_DECREF(m);
return NULL;
}
return m;
}
像先前所讲的那样PyType_Ready
初始化对象,然后通过PyModule_AddObject
方法将对象加载到模块内,以使它在被导入的时候添加对象,最后再返回模块。
现在我们看一下效果——
如果能这样就说明全部正确,离我们目标又进了一步!
小结
本篇文章介绍了C语言API的重要对象PyObject
,并展示了如何用C语言定义一个Python对象。下一章开始会讲解Python内部对象的API,并会涉及到部分虚拟机的讲解。