likes
comments
collection
share

Python C语言API系列教程(五、Python方法和模块)

作者站长头像
站长
· 阅读数 14

上一篇文章中,我分享了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_codeco_constsco_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_namem_doc分别是模块名称及其说明。m_size就是上文提到的该模块状态的大小,如果是-1则说明不涉及多解释器的状态,否则就是状态的字节数。m_methods是模块级别的方法,不通过类名调用。m_slots是该模块的插槽,一般只有在多阶段初始化的时候才有意义。

Python的模块初始化分为两种,一种是单阶段初始化,另一种是多阶段初始化。单阶段初始化就是我们现在采用的模式,直接将PyModuleDef对象传入PyModule_Create函数,并获得模块对象,这个模块对象可以作为入口函数 PyMODINIT_FUNC的返回值。整个过程通过一个函数完成,只有一个create阶段,所以叫单阶段初始化。而多阶段初始化类似于类的__new____init__的两个阶段。在第一阶段,create阶段,解释器会根据传入的PyModuleDef生成对应的模块对象,这个生成方法由m_slotsPy_mod_create函数指针完成;而在第二阶段,execute阶段,解释器会执行部分代码来初始化刚刚生成的模块对象,这部分代码由m_slotsPy_mod_exec函数指针提供。多阶段整个初始化逻辑由函数PyModuleDef_Init完成,而不是PyModule_Create函数。

现在关注一下API——

  • PyModule_Create:用单阶段初始化的方式创建一个模块对象。这个在之前讲过也用过,不再赘述。
  • PyModule_Init:初始化一个模块对象。如果要多阶段初始化的话需要将m_size设置为非负,且实现m_slots内的相关函数。
  • PyModule_AddObject:给当前模块添加对象,一般是类对象,毕竟方法可以直接写在md_methods里面。
  • PyImport_Import:导入指定模块,类似于__import__方法。这个函数比较有用,可以在代码中导入其他已经初始化好的模块。其参数是模块名称字符串,返回一个模块对象。

操作实践

在之前的几篇文章中,我们都在实现datetimecpydate对象,现在开始实现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系列教程(五、Python方法和模块)

小结

本章讲述了Python的方法对象以及模块对象,并了解了虚拟机是如何处理它们的。此外还通过它们的C语言API实现了time模块的strftime方法。

在下一章中,我们要接触C语言API的灵魂——PyBuffer。