Python C语言API系列教程(五、Python方法和模块)
在上一篇文章中,我分享了Python内置容器的结构以及相关API。此外,文章很浅显的讲解了Python虚拟机的行为以及如何利用这些API实现datetimecpy
模块。
在本篇文章中,我们会继续探讨Python的方法对象以及模块对象,并继续完善datetimecpy
模块。代码repo在这里。
要点详解
PyFunctionObject
及其相关函数
Python的设计哲学强调万物皆对象,因此Python中的方法也是一个对象,叫PyFunctionObject
。在大多数语言中,方法总是和代码片段相关联,Python也是如此。具体结构如下——
typedef struct {
PyObject_HEAD
PyObject *func_code; // PyCodePbject对象,本质上是Python源代码编译版本
PyObject *func_globals; // 方法中的全局变量
PyObject *func_defaults; // 方法的默认参数列表
PyObject *func_kwdefaults;// 方法的参数列表
...
PyObject *func_name; // 方法的名称
...
} PyFunctionObject;
来自Python 3.9
从结构可以看出,一个PyFunctionObject
对象和一个代码对象PyCodeObject
对应,执行方法的时候本质上就是运行对应的代码。
不同于C语言的函数是通过指令的跳转实现的,Python是对代码片段的直接引用。正因如此,Python和C语言是不能直接转换的,要么使用C语言API,要么借助其他工具,比如
ctype
模块(libffi实现)。
再来看看PyCodeObject
的结构——
struct PyCodeObject {
PyObject_HEAD
int co_argcount; // 参数个数
...
int co_nlocals; // 本地变量个数
int co_stacksize; // 堆栈高度
...
PyObject *co_code; // 字节码列表
PyObject *co_consts; // 常量列表
PyObject *co_names; // 变量列表
...
};
来自Python 3.9
最重要的三个参数就是co_code
、co_consts
和co_names
,都是在生成PyCodeObject
的时候生成的。co_code
很直观,就是存放的字节码及其参数,co_consts
存放的是执行代码所用到的常量,co_names
存放的是执行过程中的变量。举个例子就好理解了,当我们写下x = 1
这么一行Python代码的时候,Python会经过一系列复杂的操作将其编译成字节码,比如像下面这个样子——
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (x)
4 LOAD_CONST 1 (None)
6 RETURN_VALUE
重点关注这四行字节码,它们都存放在co_code
中。第一行LOAD_CONST 0
表示将co_consts
中下标为0的对象压入堆栈,这个例子中就是1
这个值压入堆栈。STORE_NAME 0
表示将co_names
中下标为0的对象与栈顶对象绑定,这里就是将x
与栈顶的1
绑定,即x=1
。接下来又是LOAD_CONST 1
,表示将co_consts
中第1个元素压入堆栈,这里是None
。最后是RETURN_VALUE
,表示将栈顶元素返回。
获取Python代码对应的字节码很简单,利用Python自带的
dis
模块即可。只需要执行python -m dis source.py
就可以获取对应的字节码。但需要注意的是,每个版本的字节码会略有出入,甚至格式都有改变,需要对应官方文档解读。
到这一步为止,读者应该对方法对象有一定了解了,再来看看其API——
PyFunction_New
:新建一个方法对象,用户需要提供代码对象code
和全局变量globals
。其他参数可以根据这两个参数推导。PyFunction_GetCode
:通过给定的方法对象获取它的代码对象。返回一个借引用。PyObject_Call
:根据给定的参数调用给定的对象。是一个Object
的API,因此只要实现了__call__
方法的对象都能调用。它返回一个新的引用。PyObject_CallFunctionObjArgs
:和上面类似,不过可以像Python一样直接传入参数调用给定方法,更简洁一些。它的第一个参数是被调用的对象Callable,后面的变长参数作为调用这个Callable的参数。最后一个参数为NULL
。
PyModuleObject
及其相关函数
PyModuleObject
应该是我们最早接触到的对象,因为第一篇就讲解了如何创造一个模块datetimecpy
,里面就用到了相关函数,只不过当时没有显式使用这个对象。实际上在3.9版本中,这个对象也没有暴露给用户,我们都是用PyObject
代替的。不过也不妨看一下它的结构——
typedef struct {
PyObject_HEAD
PyObject *md_dict;
struct PyModuleDef *md_def;
void *md_state;
...
} PyModuleObject;
来自Python 3.9
md_dict
是模块对象的__dict__
属性,它囊括了和该模块相关的数据,比如__name__
、__doc__
和__package__
等。md_state
描述是该模块的状态,指向一块内存空间。一般在多解释器的情况下存在(不同的解释器对应的该模块有不同的状态),如果只有一个Python解释器那就是NULL。md_def
在第一篇就接触过了,它是刻画Python模块的对象,具体结构如下——
typedef struct PyModuleDef_Base {
PyObject_HEAD
...
} PyModuleDef_Base;
typedef struct PyModuleDef{
PyModuleDef_Base m_base;
const char* m_name;
const char* m_doc;
Py_ssize_t m_size;
PyMethodDef *m_methods;
struct PyModuleDef_Slot* m_slots;
...
} PyModuleDef;
来自Python 3.9
在这个结构中,m_name
和m_doc
分别是模块名称及其说明。m_size
就是上文提到的该模块状态的大小,如果是-1则说明不涉及多解释器的状态,否则就是状态的字节数。m_methods
是模块级别的方法,不通过类名调用。m_slots
是该模块的插槽,一般只有在多阶段初始化的时候才有意义。
Python的模块初始化分为两种,一种是单阶段初始化,另一种是多阶段初始化。单阶段初始化就是我们现在采用的模式,直接将PyModuleDef
对象传入PyModule_Create
函数,并获得模块对象,这个模块对象可以作为入口函数 PyMODINIT_FUNC
的返回值。整个过程通过一个函数完成,只有一个create阶段,所以叫单阶段初始化。而多阶段初始化类似于类的__new__
和__init__
的两个阶段。在第一阶段,create阶段,解释器会根据传入的PyModuleDef
生成对应的模块对象,这个生成方法由m_slots
的Py_mod_create
函数指针完成;而在第二阶段,execute阶段,解释器会执行部分代码来初始化刚刚生成的模块对象,这部分代码由m_slots
的Py_mod_exec
函数指针提供。多阶段整个初始化逻辑由函数PyModuleDef_Init
完成,而不是PyModule_Create
函数。
现在关注一下API——
PyModule_Create
:用单阶段初始化的方式创建一个模块对象。这个在之前讲过也用过,不再赘述。PyModule_Init
:初始化一个模块对象。如果要多阶段初始化的话需要将m_size
设置为非负,且实现m_slots
内的相关函数。PyModule_AddObject
:给当前模块添加对象,一般是类对象,毕竟方法可以直接写在md_methods
里面。PyImport_Import
:导入指定模块,类似于__import__
方法。这个函数比较有用,可以在代码中导入其他已经初始化好的模块。其参数是模块名称字符串,返回一个模块对象。
操作实践
在之前的几篇文章中,我们都在实现datetimecpy
的date
对象,现在开始实现time
对象。实现这个对象的思路和之前有点不一样,这次我们直接复用官方的time
模块,并实现strftime
方法。代码仓库在这里,本章代码在Ch-5分支上。
先用Python打一个草稿——
>>> import datetimecpy
>>> datetimecpy.time.strftime('%H:%M:%S')
'21:21:39'
首先,新建一个time.h
文件,用于编写time
相关代码。不同于官方的datetime
,这次我们把time
封装成模块而不是类,并将其作为datetimecpy
的子模块。注意到这个模块包括一个模块级别的方法strftime
。
PyObject* Time_strftime(PyObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef time_methods[] = {
{"strftime", Time_strftime, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("strftime\n-\n\n Format the given time.")},
{NULL, NULL}
};
PyDoc_STRVAR(doc_time,
"time\n-\n\n\
The time implementation for datetimecpy");
static struct PyModuleDef time_def = {
PyModuleDef_HEAD_INIT,
.m_name = "time",
.m_doc = doc_time,
.m_size = 0,
.m_methods = time_methods
};
和第二章讲的类级别的方法一样,无需赘述。然后我们实现strftime
方法。这个方法是复用time.strftime
方法,也是一个模块级别的方法,所以在导入的过程中可以直接导入这个方法,类似于下面这个Python语句——
from time import strftime
这样,我们就在上下文中获得strftime
这个方法对象。因此对应的C语言API可以这样调用——
PyObject* _Time_get_module_attr(const char* mod, const char* attr) {
PyObject* pmodname = PyUnicode_FromString(mod);
if (pmodname == NULL) {
return NULL;
}
PyObject* pattrname = PyUnicode_FromString(attr);
if (pattrname == NULL) {
Py_DECREF(pmodname);
return NULL;
}
PyObject* m = PyImport_Import(pmodname);
if (m == NULL) {
return NULL;
}
PyObject* result = PyObject_GetAttr(m, pattrname);
Py_DECREF(m);
return result;
}
这是一个工具方法,目的是将C字符串转化为Python字符串后,通过PyImport_Import
导入模块,并通过PyObject_GetAttr
获取该模块的方法。
要调用这个方法只需要使用对应的方法对象的API——
PyObject* result = NULL;
PyObject* strftime = _Time_get_module_attr("time", "strftime");
result = PyObject_CallFunctionObjArgs(strftime, format, t, NULL);
由于strftime
方法接受两个参数,第一个是解析的格式format,第二个是时间timetuple,所以调用的时候直接将这两个参数作为PyObject_CallFunctionObjArgs
的参数传入,最后传入NULL
作为sentinel。
最后在扩展入口函数中添加这个模块到datetimecpy
模块中——
...
PyObject* time_mod = PyModule_Create(&time_def);
if (time_mod != NULL) {
Py_INCREF(time_mod);
if (PyModule_AddObject(m, "time", time_mod) < 0) {
Py_DECREF(time_mod);
Py_DECREF(&Date_type);
Py_DECREF(m);
return NULL;
}
}
return m;
现在运行这个模块,如果出现以下结果就说明一切正常。
小结
本章讲述了Python的方法对象以及模块对象,并了解了虚拟机是如何处理它们的。此外还通过它们的C语言API实现了time
模块的strftime
方法。
在下一章中,我们要接触C语言API的灵魂——PyBuffer。
转载自:https://juejin.cn/post/7241499876647043131