如何在C语言中实现模块化
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在C语言中实现模块化相关的知识,希望对你有一定的参考价值。
参考技术A 以函数为小模块。
以文件为中模块。
若干个文件编译成库,形成大模块。
这样一级级的
实现模块化。
使用C语言为python编写动态模块--解析python中的对象如何在C语言中传递并返回
楔子
编写扩展模块,需要有python源码层面的知识,我们之前介绍了python中的对象。但是对于编写扩展模块来讲还远远不够,因为里面还需要有python中模块的知识,比如:如何创建一个模块、如何初始化python环境等等。因此我们还需要了解一些前奏的知识,如果你的python基础比较好的话,那么我相信你一定能看懂,当然我们一开始只是介绍一个大概,至于细节方面我们会在真正编写扩展模块的时候会说。
关于使用C为python编写扩展模块,我前面还有一篇博客,强烈建议先去看那篇博客,对你了解Python底层会很有帮助。因为使用C来编写python可以直接import的模块,需要严格遵守python规定的api,而这些api是和python解释器源码保持一致的,所以我们才需要大量python源码的知识。
编写扩展模块前奏曲
好啦,我们看看编写一个扩展模块需要了解哪些东西:
初始化一个模块:
//这个XXX非常重要,这个是你最终生成的扩展模块的名字 PyInit_XXX(void); //模块初始化入口
创建一个模块
PyModule_Create(PyModuleDef *); //创建模块
模块信息
//一个结构体,通过这个结构体就可以定义一个用于生成模块的对象,里面不同的字段存储了未来生成的模块的各种信息 PyModuleDef XXX; //模块的信息有很多,比如都会有一个公共的部分,正如所有对象都有PyObject这个结构体一样 PyModuleDef_Base m_base; //这便是一个模块的公共信息,另外python中也提供了类似PyObject_HEAD的宏 #define PyModuleDef_HEAD_INIT PyModuleDef_Base m_base;//我们可以使用PyModuleDef_HEAD_INIT来代替 //模块肯定要有名字,这个名字要和上面的PyInit_XXX中的XXX保持一致 const char *m_name; //除此之外,模块还有一个文档注释,对,就是我们看到的__doc__ const char *m_doc; //模块的独立空间,但是我们一般不会使用,所以直接设置为-1即可 Py_ssize_t m_size; // -1 //一个模块里面是不是要定义大量的函数啊,所以还有一个数组,数组存放了大量的结构体 //每一个函数都是一个PyMethodDef结构体,是的,PyMethodDef也是一个结构体 //这个结构体里面肯定要保存了函数的各种信息、比如名字、doc、以及最关键的、真正指向具体的函数的指针 //这个m_methods就是存储PyMethodDef的数组的首地址 PyMethodDef *m_methods;//一个数组的首地址 //在python中PyModuleDef 结构体后面还有四个NULL,也就说一个完整的PyModuleDef对象应该长这个样子 PyModuleDef module = { PyModuleDef_HEAD_INIT, "模块名", "注释", -1, m_methods, //一个数组的首地址 NULL, NULL, NULL, NULL }; //因此在C中定义一个PyModuleDef对象就按照上面的逻辑
模块的函数信息
//我们上面说了,一个模块里面会包含很多结构体,这些结构体就是python中的函数 //因为python中的函数会有很多信息,所以底层对应的是一个结构体 PyMethodDef xxx; //一个结构体类型,里面存储了函数的各种信息 //函数和模块一样也是有名字的 const char *ml_name; //当然啦,肯定还有一个指针,这个指针指向的才是真正的函数 //这些函数都是一个PyCFunction PyCFunction *ml_methd; //函数还有参数类型: //METH_VARARGS:存在*args //METH_KEYWORDS:存在**kwargs //METH_NOARGS:不接受参数 //METH_O:接收一个参数 int ml_flags; //函数注释 const char *ml_doc; //因此在底层定义一个python中的函数,那么PyMethodDef对象长这个样子 PyMethodDef f = { "函数名", (PyCFunction)func, //我们在C中定义的函数,这里会得到一个指针,我们记得转换成PyCFunction,但是不转也不会有问题,最好还是转一下 METH_O, //函数类型 "函数注释" }; //下面就是最关键的函数了,这个返回一个PyObject *,至少要接收一个PyObject * static PyObject * func1(PyObject *self, PyObject *args) { } //如果是带有默认参数的话,就是 static PyObject * func2(PyObject *self, PyObject *args, PyObject *dictionary) { }
以上是一些前奏知识,先有一个概念,细节我们会在编写代码的时候通过注释来介绍,通过编写代码很快就能明白。我们说C编写完之后,肯定要编译成python的扩展模块,那么如果编译呢?
from distutils.core import *
setup(name="egg_info中的元信息显示的模块名",
# 注意:我们在编译完之后,会有一个egg文件,里面显示了模块的元信息,这个是egg文件里面的信息
verison="1.0",
# 接收多个Extension("模块名", ["C文件"])
ext_modules=[Extension("hanser", ["C文件"])]
)
假设这个文件叫做setup.py,那么打包成扩展库就可以通过python setup.py install来实现。
编写一个简单的扩展模块
下面我们来编写一个简单的扩展模块,生成的扩展模块的名字我们就叫hanser吧,也就是最终可以让python解释器通过import hanser
来导入。至于我们定义的C文件叫什么名字都无所谓,我们叫做a.c吧,简单粗暴一些。
//编写python扩展模块,需要引入Python.h这个头文件
//这个头文件,在python安装目录的include目录下,编译的时候会自动寻找
#include "Python.h"
//我们先来定义几个函数,只需要知道有这么些函数就行,每一个函数都要返回一个PyObject *
//而且至少要接收一个PyObject *self,但是不好意思对于python来说这个函数是没有参数的,这个self是必须的
static PyObject *
my_func1(PyObject *self)
{
//函数返回一个PyObject *,我们说python中所有对象在底层对应的结构体都包含了PyObject,所以我们返回一个整型、字符串都是可以的
//这里返回一个整型,比较简单。但是直接return 100;可以吗?我们说100是C中的整型,但不是python中的整型
//所以我们需要转一下,我们在上一篇博客中介绍了python中的对象和C中的对象是如何进行转化的,如果不知道规则可以去看一下
return PyLong_FromLong(100);
//返回python中int,此时对象的内存由python管理,此时就跟在python中创建一个整型是类似的
}
static PyObject *
my_func2(PyObject *self, PyObject *a)
{
//加上100
long _a = PyLong_AsLong(a);
a = PyLong_FromLong(_a + 100);
return a;
}
//4.第四个步骤(1和2和3在下面)
/*
定义一个结构体数组,类型为PyMethodDef,里面存储了结构体,这个结构体里面存放了函数的名字、注释、指向了函数的指针
数组名字叫什么无所谓
*/
static PyMethodDef module_functions[] = {
//这个里面就可以创建了
{
"my_func1", //函数名称
(PyCFunction)my_func1, //函数指针,所以我们需要定义一些函数
METH_NOARGS, //函数参数标识,这里没有参数
"this is a function named my_func1", //函数文档注释
},
//一个模块可以有很多函数
{
"my_func2",
(PyCFunction)my_func2,
METH_O,
"this is a function named my_func2",
},
//当函数(结构体)定义完了,最后要有{NULL, NULL}
{NULL, NULL}
};
//3.第三个步骤
/*
定义一个生成模块的对象吧,还记得在C中怎么定义吗?对的,通过PyModuleDef
我们定义的变量叫什么无所谓,但是一般都保持一致,所以这里我们也叫hanser
但是不叫hanser也无所谓,为了区分演示,我们这里就叫别的名字,就叫HANSER吧,改成大写
*/
static PyModuleDef HANSER = {
//记得,要有一个头部
PyModuleDef_HEAD_INIT,
//然后是模块名
"hanser",
//模块的注释,或者说是文档说明,如果不需要的话,写成NULL
"this is a module named hanser",
//然后是模块的空间,即使那个m_size,我们说这个不需要关心,直接写成-1即可
-1,
//然后是函数、准确的说应该是结构体,数组的地址了
module_functions,
//别忘了下面的四个NULL
NULL,
NULL,
NULL,
NULL
};
//1.第一个步骤:
/*
扩展库入口函数
这是一个宏,python的源代码我们知道是使用C来编写的
但是编译的时候为了支持C++的编译器也能编译,于是需要通过extern "C"定义函数
然后这样C++编译器在编译的的时候就会按照C的标准来编译函数
这个宏就是干这件事情的,主要和python中的函数保持一致
没必要太深究,写上就行
*/
PyMODINIT_FUNC
//2.第二个步骤
/*
模块初始化入口,PyInit_XXX(void),XXX是我们定义的模块名,这里叫hanser
*/
PyInit_hanser(void)
{
//打印一句话吧,我们的Python.h中已经引入了stdio.h这个头文件了,所以可以直接打印
printf("%s
", "PyInit_hanser");
//创建python中的模块,将使用PyModuleDef定义的模块对象的指针传递进去,然后返回得到python中的模块
return PyModule_Create(&HANSER);
}
from distutils.core import *
setup(
# 打包之后会有一个egg_info,表示该模块的元信息信息,name就表示打包之后的egg文件名
# 但它并不是我们的模块名,这两个名字不一样也可以,但是我们一般都会写一样的,这样才知道你是哪个模块的
# 不信我们改一下,改成hanser1
name="hanser1",
version="10.22", # 版本号
author="古明地觉", # 作者
author_email="东方地灵殿", # 作者邮箱
# 关键来了,这里面接收一个类Extension,类里面传入两个参数,第一个参数还是我们的模块名,必须和PyInit_XXX中的XXX保持一致,否则报错
# 我们发现貌似好像指定了好多的名字,总之一句话:PyInit_XXX中的XXX、我们定义PyModuleDef对象中的m_name、以及这里的Extension里面的第一个参数要保持一致
# 它们就是模块名
# 第二个参数还是一个列表,表示用到了哪些C文件,因为扩展模块对应的C文件不一定只有一个
ext_modules=[Extension("hanser", ["a.c"])]
)
我们的py文件名就叫做1.py,然后我们在控制台输入python 1.py?install
。
我们看到打包成功,并且它还自动的帮我们移到了site-packages
里面,我们来看看。
我们看到了一个hanser.pyd文件,至于中间的部分就是解释器版本,然后我们下面的egg-info,我们注意到这是hanser1,我们再打开看看。
我们看到里面的Name就是我们在setup函数中指定的name,我们起名为hanser1,我们说那个name是生成的egg-info的名字、或者说里面存储的元信息显示的名字,但它不是模块名。但即便如此,也不要改成其他的名字,因为egg-info就是描述模块的元信息的,然后你egg-info的名字不就代表了你要描述哪个模块的元信息吗?要是把名字改成其他的,容易造成困惑。
所以我们看到就比较讨厌,里面需要指定的名字太多了。但是虽然多,我们还是那句话记住一点:PyInit_XXX的XXX、PyModuleDef定义的模块中的m_name、Extension中的第一个参数、还有setup中name参数都保持一致即可、至于名字是什么,你要生成的扩展模块叫什么、它们就叫什么。至于其它的变量名就没有要求了,假设我们要生成的模块名叫yousa。那么:
PyInit_yousa
m_name:yousa
PyMethodDef xx = { ..., "yousa", ... }
setup(name="yousa", ext_modules=[extension("yousa", ["xx.c"])])
我们说,编译好的扩展模块直接自动拷贝到site-packages
中了,那么我们是可以直接import的。除此之外,还在当前目录中生成了build目录,我们看到了hanser.pyd,那个也是可以直接导入的。
import hanser
print(hanser.my_func1())
print(hanser.my_func2(20))
"""
100
120
PyInit_hanser
"""
# 我们在PyInit_hanser中打印了一句话,在导入模块的时候也打印了出来
# 别看它是最后打印的,其实在我import hanser的时候就会打印
# 只不过因为缓冲区的原因,它最后输出的
最后再看一个神奇的东西,我们知道在pycharm这样的智能编辑器中,通过Ctrl加左键可以调到指定模块的指定位置。
神奇的一幕出现了,我们点击进去居然还能跳转,其实我们在编译成扩展模块移动到site-packages
中,pycharm就自动生成了。我们看到模块注释、函数的注释跟我们在C文件中指定的一样。
我们把代码放到linux上也是可以完美编译、执行的,此时的printf("%s
", PyInit_hanser)
是先打印的,不过这才是正常结果。因此此时我们就编写了一个简单的扩展模块,下面来总结一下流程。
编写扩展模块流程总结
第一步:include "Python.h",必须要引入这个头文件,这个头文件中还引入了C中的一些标准库,具体都引入了哪些库我们可以查阅。当然如果不确定但又懒得看,我们还可以手动再引入一次,反正include同一个头文件只会引入一次。
第二步:理论上这不是第二步,但是按照编写代码顺序我们就认为它是第二步吧,对,就是按照我们上面写的代码从上往下撸。这一步你需要编写函数,这个函数就是C语言中定义的函数,这个函数返回一个PyObject *,至少要接收一个PyObject *,我们一般叫它self,这第一个参数你可以看成是必须的,无论我们传不传其他参数,这个参数是必需要有的。所以如果只有这一个参数,那么我们就认为这个函数不接收参数,因为我们在调用的时候没有传递。
static PyObject * f1(PyObject *self) { } static PyObject * f2(PyObject *) { } static PyObject * f3(PyObject *) { } //假设我们定义了这三个函数吧,三个函数都不接受参数 //至于接收复杂参数,比如python中的扩展参数*args、**kwargs怎么传递 //我们后面会详细介绍
第三步:定义一个PyMethodDef类型的数组,这个数组也是我们后面的PyModuleDef对象中的一个参数,这个数组名字叫什么就无所谓了。至于PyMethodDef,它接收参数如下。另外我们可以单独使用PyMethodDef创建对象,然后将变量写到数组中,也可以直接在数组中创建,如果是直接在数组中创建的话,那么就不需要再使用PyMethodDef定义了,直接在{}里面写成员信息即可。
static PyMethodDef module_functions[] = { { //函数名,你在C中定义的函数名和这里的字符串指定的名字要一致 "f1", //函数指针,最好使用PyCFunction转一下,可以确保不出问题。 //如果不转,我自己测试没有问题,但是编译时候会给警告,最好还是按照标准,把指针的类型转换一下 //转换成python底层识别的PyCFunction (PyCFunction)f1, METH_NOARGS, //参数类型,这里不接收参数,至于怎么接收*args和**kwargs的参数,后面说 "函数f1的注释" }, {"f2", (PyCFunction)f2, METH_NOARGS, "函数f2的注释"}, {"f3", (PyCFunction)f3, METH_NOARGS, "函数f3的注释"}, //别忘记,下面的{NULL, NULL},不要问我为什么,python源码就是这么写的 {NULL, NULL} }
第四步:定义PyModuleDef对象,这个变量的名字叫什么也没有要求。
static PyModuleDef m = { PyModuleDef_HEAD_INIT, //头部信息 //模块名,这个是有讲究的,你要编译的扩展模块叫啥,这里就写啥 //假设叫yousa吧,那么这里就写yousa "yousa", "模块的注释", -1, //模块的空间,这个是给子解释器调用的,我们不需要关心,直接写-1即可 module_functions, //然后是我们上面定义的数组名,里面放了一大堆的PyMethodDef结构体实例 //然后是四个NULL,还是不要问我为什么,源码就是这么写的,我们这么写也没错 NULL, NULL, NULL, NULL }
第五步:写上一个宏,其实把它单独拆分出来,有点小题大做了。
PyMODINIT_FUNC //一个宏,主要是保证函数按照C的标准,不用在意,写上就行
第六步:按理说这是第二步的,不过无所谓啦。创建一个模块的入口函数,我们说编译的扩展模块叫yousa,那么这个函数名就要这么写
PyInit_yousa(void) { //我们上面打印了一句话,但是生产中是没有必要的,所以直接根据上面定义的PyModuleDef实例,得到python中的模块 //PyModule_Create就是用来创建python中的模块的,直接将PyModuleDef定义的对象的指针扔进去 //便可得到python中的模块,然后直接返回即可。 return PyModule_Create(&m); }
第七步:定义一个py文件,假设叫xx.py,那么在里面写上如下内容,然后python xx.py install即可
from distutils.core import * setup( # 这是生成的egg文件名,也是里面的元信息中的Name name="yousa", # 版本号 version="10.22", # 作者 author="古明地觉", # 作者邮箱 author_email="东方地灵殿", # 当然还有其它参数,作为元信息来描述模块,比如description:模块介绍。有兴趣的话可以看函数的注释,或者根据已有的egg文件自己查看 # 下面是扩展模块,Extension("yousa", ["C源文件"]) # 我们说Extension里面的第一个参数也必须是你的扩展模块的名字,并且必须要和PyInit_XXX以及PyModuleDef中m_name保持一致 # 至于第二个参数就是一个列表,你需要用到哪些C源文件。 # 而且我们看到这个Extension也在一个列表里面,因为我们也可以传入多个Extension同时生成多个扩展模块。但是一般我们是写好一个生成一个,你也可以一次性写多个,然后只编译一次。 ext_modules=[Extension("hanser", ["a.c"])]
难点
我们看到编写一个扩展模块是有套路的,只要把上面的流程记好就没问题。难点在哪里呢?其实难点主要在函数的编写上面,比如python和C之间的类型互转,再比如我们后期传递*args
,这在底层是一个PyTupleObject,我们还需要调用PyTupleObject的api来进行元素的解包。所以难点就在于函数的编写上面,因为要大量使用python底层的api。
再比如两个字符串相加,那么python中直接通过+就可以实现了,但是在底层你需要调用PyUnicode_Concat
函数,因为python的底层就是这么干的,所以你也要这么干。所以我在上一篇博客中就说过,编写扩展模块需要有python源码的知识,因为你需要大量使用python底层的api,也许在python中对于*args
,你可以很简单的就操作了,但是在底层你可能就需要多做一些事情。因此我一直强调要有python源码方面的知识,只有这样才能编写出复杂的扩展模块,如果仅仅是传递简单地数字、字符串,那编写扩展模块的意义何在呢?
因此可以多看看python底层的api,我之前也说过,python中api和底层的api是比较类似的,模式比较固定。比如我们对列表进行元素的修改,在底层就是PyList_SetItem
,获取元素就是PyList_GetItem
,因此如果不会的话可以去查一查。我们说源码的Include文件是放一些对象的定义,至于这些对象支持哪些操作都放在Objects目录下,而Python目录则是存放一些和虚拟机执行流程相关的。然后根据使用python的经验来猜测底层对应哪些函数,直接Ctrl+F5通过关键字查询,或者直接百度、谷歌也可以。当然我在下面,也会尽可能多介绍一些api。
与PyObject的再度重相逢
我们在上一篇中介绍了PyObject,我们说这里面存放了引用计数和类型,并且python中所有对象底层对应的结构体都嵌套了PyObject,因此python中的所有对象都有引用计数和类型。并且python的对象在底层,都可以看成是PyObject的一个扩展,都可以把类型转成PyObject。
PyObject *
PyLong_FromLong(long ival)
{
PyLongObject *v;
...
return (PyObject *)v;
}
我们看到这个函数是把C中long转成PyLongObject,但是我们发现在返回的时候将PyLongObject?*
转成了PyObject?*
,因此我们定义函数的时候明明接收的是int,但是我们却不定义成PyLongObject?*
,而是定义成PyObject?*
,因为python底层的好多api接收都是PyObject?*
。如果在函数执行的时候需要指定明确的类型,那么再转回来即可。
我们扯到PyObject上面来,还有一个原因,那就是引用计数。不过python专门定义了几个宏,我们来看一下:
#define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt)
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
#define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
Py_REFCNT:拿到对象的引用计数;Py_TYPE:拿到对象的类型;Py_SIZE:拿到对象的ob_size,也就是变长对象里面的元素个数。
除此之外,python还提供了两个宏:Py_INCREF和Py_DECREF来用于引用计数的增加和减少。
//引用计数增加很简单,就是找到ob_refcnt,然后++
#define Py_INCREF(op) ( _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA ((PyObject *)(op))->ob_refcnt++)
//但是减少的话,做的事情稍微多一些
//其实主要就是判断引用计数是否为0,如果为0直接调用_Py_Dealloc将对象销毁
//_Py_Dealloc也是一个宏,会调用对应类型对象的tp_dealloc,也就是析构方法
#define Py_DECREF(op) do { PyObject *_py_decref_tmp = (PyObject *)(op); if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA --(_py_decref_tmp)->ob_refcnt != 0) _Py_CHECK_REFCNT(_py_decref_tmp) else _Py_Dealloc(_py_decref_tmp); } while (0)
python向扩展模块传递多个参数
我们说PyMethodDef定义的结构体有一个属性:ml_flags,这是一个int类型,它代表的函数的参数类型。
METH_VARARGS:传递多个位置参数
METH_KEYWORDS:传递关键字参数
METH_NOARGS:不接收参数
METH_O:接收一个参数
我们说如果指接收一个参数,那么使用METH_O
即可,如果接收多个参数呢?那么就使用METH_VARARGS
,那么根据python的经验我们知道会得到一个元组,所以我们下面就要学习如何在C中对一个元组进行解包。另外, 如果只接收一个参数,我们还是可以把参数类型写成METH_VARARGS
的,只不过此时的元组只有一个元素,一般情况下为了保证代码的通用性,我们都会写成METH_VARARGS
。
我们说,如果是多个参数,那么得到的是一个元组,我们需要将参数从元组中解析出来。所使用的函数就是:int PyArg_ParseTuple(PyObject *args, const char *format, ...)
,我们注意到format是一个格式,类似于printf,里面肯定是一些占位符,那么都支持哪些占位符呢?
s:将const char*转成python中的str或者None,使用utf-8编码
y:将const char*转成python中的bytes或者None,使用utf-8编码
u:将const wchar_t*转成python中unicode,编码为utf-16或者ucs4
i:将C中的int转为python中int
b:将C中的char转为python中int
l:将C中的long转为python中int
? 关于int,种类比较多,可以自己查看
f:将C中的float转成python的float
d:将C中的double转成python的float
O:将PyObject *转成python的object
至于其他的占位符,有兴趣可以自己搜索,并不一定所有的占位符都会用到。下面我们就来编写代码来实践一下, 此时的代码就不会每一行都有大量的注释了,如果是之前写过的就不写了,但是新出现的还是会详细解释的。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
//目前我们定义了一个PyObject *args,如果传递一个参数,那么这个args就是对应的一个参数
//如果接收多个参数,还是只需要定义一个*args即可,只不过此时的*args是一个PyTupleObject,我们需要将多个参数解析出来
//此时我们这个函数是接收两个int,然后相加
int a, b;
//下面我们需要使用PyArg_ParseTuple进行解析,因为我们接收两个参数
//这个函数返回一个整型,如果失败会返回0,成功返回非0
//我们在python中传递过来的就是PyLongObject,这里会把a和b当成PyLongObject去解析
//然后把python传递过来的值交给a和b,但是a和b本质上还是一个int,只不过它们拿到了python传递过来的值
if (!PyArg_ParseTuple(args, "ii", &a, &b)){
//失败返回NULL,后面我们会介绍如何在底层返回一个异常
return NULL;
}
//返回相加的结果
return PyLong_FromLong(a + b);
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS, //这里要把参数类型改成METH_VARARGS
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
这里编译的过程我们就不显示了,跟之前是一样的。并且为了方便,我们的模块名就不改了,还叫hanser。但是编译之后的pyd文件内容已经变了,不过需要注意的是,我们说编译之后会有一个build目录,然后会自动把里面的pyd文件拷贝到site-packages中,如果你修改了代码,但是模块名没有变的话,那么编译之后的文件名还和原来一样。如果一样的话,那么它发现已经存在相同文件了,就不会再拷贝了。因此两种做法:要么你把模块名给改了,这样编译会生成新的模块。要么编译之前记得把上一次编译生成的build目录先删掉,我们推荐第二种做法,不然site-packages目录下会出现一大堆我们自己定义的模块。
import hanser
try:
print(hanser.my_func1())
except Exception as e:
# 我们看到,由于我们在解析的时候写了两个i,那么python解释器知道需要传递两个参数
# 这里这里报错了,当然后面我们也会自己在底层返回异常
print(e) # function takes exactly 2 arguments (0 given)
try:
print(hanser.my_func1("xx", 123))
except Exception as e:
# 还是那句话,我们解析的时候写的是两个i,那么解释器知道需要传递的都是整型
print(e) # an integer is required (got type str)
# 成功执行
print(hanser.my_func1(10, 25)) # 35
类型检查和返回异常
在python中,当我们传递的类型不对会报错。那么在底层我如何才能检测传递过来的参数是不是想要的类型呢?可以使用PyXxx_Check,比如检测是不是float,那么就是PyFloat_Check。那么检测其他的类型,我想你一定知道如何举一反三。
如何返回一个异常呢?返回异常之前,我们需要知道python中的异常在底层对应什么?规则也很简单,比如TypeError,那么在底层就对应PyExc_TypeError,也就是说在前面加上一个PyExc即可。异常有了如何设置呢?有两种方式:PyErr_SetString和PyErr_Format
//我们这里就只把函数实现贴上去,其它的就不贴了, 因为是一样的
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
//我们说args可以接收一个参数,那么该参数就是args
//记得下面的PyMethodDef实例的参数类型改成METH_O,表示接收一个参数
if (!PyFloat_Check(args)){
//我们说Py_TYPE是一个宏可以拿到对应的类型的指针,然后通过->tp_name就能够拿到类型名
//比如:PyLongObject *args, 那么Py_TYPE(args)就是&PyLong_Type,调用->tp_name拿到的就是字符串"int"
PyErr_Format(PyExc_TypeError, "参数传递错误,我们需要float,但是你传了%s
", Py_TYPE(args)->tp_name);
//如果不需要占位符,那么通过PyErr_SetString即可。接收一个异常类型和描述异常的字符串
return NULL;
}
//给传来的值加上一个3.5
double a = PyFloat_AsDouble(args);
a = a + 3.5;
return PyFloat_FromDouble(a);
}
import hanser
try:
print(hanser.my_func1(1))
except Exception as e:
print(e) # type error,we need a float, but you pass a int
# 正常打印
print(hanser.my_func1(1.)) # 4.5
PyUnicodeObject的传递
下面我们来看看python中的字符串如何返回。
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
//我们下面是的函数参数类型是METH_VARARGS,说明这是个元组,那么元组为空也是可以的
//但是如果是METH_O,那么就必须传递一个参数了
wchar_t *s = L"古明地觉1234";
//这里我们直接返回,PyUnicode_FromWideChar表示将宽字符转成PyUnicodeObject,但是除了宽字符还需要有一个长度
int len = wcslen(s) + 1; //宽字符计算要使用wcslen,头文件是wchar.h,但是Python.h中已经引入了,由于 所以要多加个1
return PyUnicode_FromWideChar(s, len);
}
import hanser
print(hanser.my_func1()) # 古明地觉1234
然后试试如何传递字符串
#include "Python.h"
#include <locale.h>
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
//为了支持各种符号,记得加上这一句
setlocale(LC_COLLATE, "en_US.UTF-8");
//定义函数,接收两个字符串,然后合并在一起
wchar_t *s1;
wchar_t *s2;
//我们看到这是两个指针,即便如此我们依旧需要传递地址,那么空间谁来分配
//不用想,肯定是在PyArg_ParseTuple中就分配了,而且这个空间是指向python内部的空间
//这里使用占位符u,如果python传递的是纯ascii字符串的话,那么也可以用s,但是为了支持中文以及其它符号,所以这里使用u
PyArg_ParseTuple(args, "uu", &s1, &s2);
//将两个字符串合并在一起,我们可以先转成PyObject *,然后使用PyUnicode_Concat合并
//也可以直接在C中先将两个宽字符合并在一起,然后再转成PyObject *,这里我们选择后一种
//计算长度,因为 ,所以多需要一个空间
int len = wcslen(s1) + wcslen(s2) + 1;
wchar_t s[len];
wcscat(s, s1);
wcscat(s, s2);
return PyUnicode_FromWideChar(s, len);
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS,
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
print(hanser.my_func1("嘿嘿", "哈哈")) # 嘿嘿哈哈
print(hanser.my_func1("啊~雪莉", "我想死你了(吸溜?(? ???ω??? ?)?)")) # 啊~雪莉我想死你了(吸溜?(? ???ω??? ?)?)
控制多个参数的类型
现在我们知道怎么传递参数了,并且还会定义异常。但是我们仔细想一下,我们在使用PyArg_ParseTuple解析的时候,我们怎么知道占位符应该有多少个呢?假设用户参数传递少传了或者多传了,那么解析就出问题了。因此如果用户传递的参数个数不对,应该直接报出参数个数错误。
这是一方面,假设我需要三个参数,第一个参数要求是int、第二个要求是str、第三个要求是float。如果用户是按照这种逻辑传递的还好办,如果它传递的类型错误了,我们还按照对应的模式来解析肯定会出错。所以有一种办法是都解析成PyObject,还记得占位符吗?对的,使用大写的字母O。一般我们都会通过PyObject来进行解析。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args)
{
//接收三个参数
int ob_size = Py_SIZE(args);
if (ob_size != 3){
PyErr_Format(PyExc_TypeError, "function my_func1 need 3 arguments, but got %d", ob_size);
return NULL;
}
//创建3个PyObject *
PyObject *obj1;
PyObject *obj2;
PyObject *obj3;
//创建的是指针,还是传入指针,所以传入的相当于是二级指针
//因为我们说python中的变量对应底层都是指针,这样才会给PyObject *类型的obj1、obj2、obj3赋值
//怎么赋值,创建对应的PyObject对象,然后将PyObject对象的指针给obj1、obj2、obj3
PyArg_ParseTuple(args, "OOO", &obj1, &obj2, &obj3);
//我们说三个参数分别接收int、str、float,那么把它们的类型取出来,依次比较
//使用的方法是strcmp,如果相等返回0,否则返回非0,
int arg1 = strcmp(Py_TYPE(obj1) -> tp_name, "int") == 0;
int arg2 = strcmp(Py_TYPE(obj2) -> tp_name, "str") == 0;
int arg3 = strcmp(Py_TYPE(obj3) -> tp_name, "float") == 0;
if (!arg1){
PyErr_Format(PyExc_TypeError, "arg1 need a int,but you pass a %s", Py_TYPE(obj1) -> tp_name);
} else if (!arg2){
PyErr_Format(PyExc_TypeError, "arg2 need a str,but you pass a %s", Py_TYPE(obj2) -> tp_name);
} else if(!arg3){
PyErr_Format(PyExc_TypeError, "arg3 need a float,but you pass a %s", Py_TYPE(obj3) -> tp_name);
} else {
return PyUnicode_FromWideChar(L"参数类型传递正确", 9);
}
return NULL;
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS,
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
try:
print(hanser.my_func1())
except Exception as e:
print(e) # function my_func1 need 3 arguments, but got 0
try:
print(hanser.my_func1(1))
except Exception as e:
print(e) # function my_func1 need 3 arguments, but got 1
try:
print(hanser.my_func1(1, 2))
except Exception as e:
print(e) # function my_func1 need 3 arguments, but got 2
try:
print(hanser.my_func1("", "", ""))
except Exception as e:
print(e) # arg1 need a int,but you pass a str
try:
print(hanser.my_func1(1, "", ""))
except Exception as e:
print(e) # arg3 need a float,but you pass a str
try:
print(hanser.my_func1(1, "", 1.)) # 参数类型传递正确
except Exception as e:
print(e)
传递关键字参数
传递关键字参数的话,那么我们可以通过key=value
的方式来实现,那么在C中我们如何解析呢?既然支持关键字的方式,那么是不是也可以实现默认的参数,就是我们不传会使用默认值呢?答案是支持的,我们知道解析位置参数是通过PyArg_ParseTuple
,那么解析关键字参数是通过PyArg_ParseTupleAndKeywords
//函数原型
int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)
我们看到相比原来的PyArg_ParseTuple,多了一个kw和一个char*类型的数组,具体怎么用我们在编写代码的时候说。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
//我们说函数既可以通过位置参数、还可以通过关键字参数传递,那么函数的参数类型就要变成METH_VARARGS | METH_KEYWORDS
//假设我们定义了三个参数,name、age、place,这三个参数可以通过位置参数传递、也可以通过关键字参数传递
char *name = ""; //姓名
int age = 17; //年龄
char *place = "东方地灵殿"; //居住地
//可能有人注意了,我们之前使用的是wchar_t表示宽字符
//但是使用char *也可以支持中文,至少在python层面是支持的,那么我们就使用char吧
//使用wchar_t会很麻烦,既然是char *,那么我们的占位符就不用u了,而是使用s
//告诉python解释器,参数的名字,注意:这里面字符串的顺序就是函数定义的参数顺序
//也是keys后面的变量顺序,其实变量名字叫什么无所谓,但是类型要和format中对应的占位符匹配,只是为了一致我们会起相同的名字
//注意结尾要有一个NULL,否则会报出段错误。
char *keys[] = {"name", "age", "place", NULL}; //这个NULL很重要
//解析参数,我们看到format中本来应该是sis的,但是中间出现了一个|
//这就表示|后面的参数是可以不填的,如果不填会使用我们上面给出的默认值
//因此这里name就是必填的,因为它在|的前面,而age和place可以不填,如果不填就用我们上面给出的默认值
//keys就是定义的参数的名字,后面把参数的指针传进去
if (!PyArg_ParseTupleAndKeywords(args, kw, "s|is", keys, &name, &age, &place)){
return NULL;
}
char ret[100];
//格式化字符串
sprintf(ret, "你的名字是:%s, 年龄是:%d, 居住地是:%s", name, age, place);
//这里不再从宽字符来转了,应该是FromString,就是从C中字符串转成python中的字符串
//而且这个函数只需要字符数组,不需要再指定字符的个数了,很方便
return PyUnicode_FromString(ret);
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS, //参数类型要改成这个
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
try:
# 我们在C中写的是s|is,表示后面的两个参数如果不填是默认的
# 填了就是我们指定的
print(hanser.my_func1("古明地觉")) # 你的名字是:古明地觉, 年龄是:17, 居住地是:东方地灵殿
except Exception as e:
print(e)
try:
# 也可以手动指定关键字
print(hanser.my_func1(name="古明地恋")) # 你的名字是:古明地恋, 年龄是:17, 居住地是:东方地灵殿
except Exception as e:
print(e)
try:
# 我们说name是必须传递的
print(hanser.my_func1())
except Exception as e:
print(e) # Required argument 'name' (pos 1) not found
try:
# xxx显然不在char *key[]中
print(hanser.my_func1("古明地觉", xxx=123))
except Exception as e:
print(e) # 'xxx' is an invalid keyword argument for this function
try:
# 同理我们可以手动指定age
print(hanser.my_func1("古明地恋", age=15))
except Exception as e:
print(e) # 你的名字是:古明地恋, 年龄是:15, 居住地是:东方地灵殿
try:
# 也可以全部使用关键字参数指定
print(hanser.my_func1(name="椎名真白", age=17, place="樱花庄")) # 你的名字是:椎名真白, 年龄是:17, 居住地是:樱花庄
except Exception as e:
print(e)
try:
# 多传递一个参数呢?也会提示我们参数传递错误
print(hanser.my_func1("椎名真白", 17, "樱花庄", 123))
except Exception as e:
print(e) # function takes at most 3 arguments (4 given)
try:
# 参数类型不对,也会提示我们,因为我们在占位符中指定的是sis
# 第三个占位符是s,不是i,因此传递的123就不符合类型,所以python也会提示我们参数传递的不对
print(hanser.my_func1("椎名真白", 17, 123))
except Exception as e:
print(e) # argument 3 must be str, not int
我们就是实现了支持关键字参数的函数,我们说使用PyArg_ParseTupleAndKeywords的时候,里面写上了args和kw,这表示既支持位置参数、也支持关键字参数。至于顺序就是代码中的顺序,我们看到如果参数传递的个数不正确,python会自动提示你。
因此为了保证python中传参的习惯,我们会选择使用METH_VARARGS?|?METH_KEYWORDS
这种方式,表示两种都支持。而且使用这种方式最大的两个好处就是:传递的参数的个数和函数接收的个数不相同,那么python会直接告诉你参数传递个数有问题,不需要我们再通过ob_size来判断了;还有参数类型必须和占位符中指定的类型匹配,否则也会直接提示我们第几个参数类型错误,就不用再通过占位符全部指定为O、获取PyObject *,然后再拿到对应类型的tp_name来一个一个判断了。
返回布尔类型和None
我们说函数都必须返回一个PyObject *,如果这个函数没有返回值,那么在python中实际上返回的是一个None,但是我们不能返回NULL,None和NULL是两码事。在扩展函数中,如果返回NULL就表示这个函数执行的时候,不符合某个逻辑,我们需要终止掉,不能再执行下去了。这是在底层,但是在python的层面,你需要告诉使用者为什么不能执行了,或者说底层的哪一行代码不满足条件,因此这个时候我们会在return NULL之前需要手动设置一个异常,这样在python代码中就会报错,才知道为什么底层函数退出了。当然有时候会自动帮我们设置,比如上面的参数解析错误。
那么在底层如何返回一个None呢?既然要返回我们就需要知道它的结构是什么。
# 首先在python中,None也是有类型的
print(type(None)) # <class 'NoneType'>
# 这个NoneType在底层对应的是_PyNone_Type
# 至于None在底层对应的结构体是_Py_NoneStruct,所以我们返回的时候应该返回这个结构体的指针
# 不过官方不推荐直接使用,而是给我们定义了一个宏
# #define Py_None (&_Py_NoneStruct),我们直接返回Py_None即可。
"""
不光是None,我们说还有True和False
True和False对应的结构体是:_Py_FalseStruct, _Py_TrueStruct,它们本质上是PyLongObject
python也不推荐直接返回,也是定义了两个宏
#define Py_False ((PyObject *) &_Py_FalseStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)
推荐我们使用Py_False和Py_True
"""
下面我们来试一下:
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
//接收一个名为age的int,范围要求大于0,否则报错
int age = 18;
//这里即便只有一个参数,为了能够支持python函数的传递方式,我们会一直使用PyArg_ParseTupleAndKeywords来解析
char *keys[] = {"age", NULL}; //还是那句话别忘记NULL
//age如果不传,默认是18,所以占位符要设置成|i
if (!PyArg_ParseTupleAndKeywords(args, kw, "|i", keys, &age)){
//解析出错直接返回,否则程序会继续往下走
//而参数解析出了问题,python会自动报出错误,这个不需要我们关心
//所以直接return一个NULL即可
return NULL;
}
if (age <= 0){
//这个时候不符合我们逻辑而结束程序,就需要我们手动设置一个异常了
//至于像参数解析、参数类型,如果不符合指定的占位符,python会帮我们返回异常信息
//但是像这种与我们的逻辑不符,我们在结束之前肯定要设置异常,不然莫名退出了,别人也不知道哪里出问题了
PyErr_SetString(PyExc_ValueError, "age must be greater than 0");
//或者设置的更详细一些,还可以这么写
// PyErr_Format(PyExc_ValueError, "age must be greater than 0, but got %d", age);
//让函数退出,不往下执行了
return NULL;
} else if (age < 14) {
//年龄太小不行,会死刑的
//返回False,坚决表名立场
return Py_False;
} else if (age < 18) {
//这个就看情况啦,兴许只是三年呢?
//返回None,表示我也不知道会咋样
return Py_None;
} else if (age < 30) {
//这个就没问题了吧
return Py_True;
} else {
//坚决表名立场,年纪太大
return Py_False;
}
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS, //参数类型要改成这个
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
try:
# 我们不传会使用默认值
print(hanser.my_func1()) # True
except Exception as e:
print(e)
try:
# 类型错误
print(hanser.my_func1(""))
except Exception as e:
print(e) # an integer is required (got type str)
try:
# 参数个数错误
print(hanser.my_func1(15, ""))
except Exception as e:
print(e) # function takes at most 1 argument (2 given)
try:
# 传递了不存在的参数
print(hanser.my_func1(name=15))
except Exception as e:
print(e) # 'name' is an invalid keyword argument for this function
try:
# 这个时候参数解析就不会出问题了,因为-1是一个int
# 但是它不符合我们的逻辑,因此底层函数退出,但是这时候我们就需要手动设置一个异常了
# 来告诉使用者,为什么函数退出了
print(hanser.my_func1(-1))
except Exception as e:
print(e) # age must be greater than 0
try:
# age < 14,返回False
print(hanser.my_func1(age=13))
except Exception as e:
print(e) # False
try:
print(hanser.my_func1(16))
except Exception as e:
print(e) # None
try:
print(hanser.my_func1(age=18))
except Exception as e:
print(e) # True
try:
print(hanser.my_func1(age=35))
except Exception as e:
print(e) # False
PyTupleObject
传递PyTupleObject
下面我们就传递元组了,我们说像int、str这些在C的层面上是可以直接解析的,因为C中原生支持这些类型,比如:int、long、char *等等。但是像python中的元组、列表、字典、集合啊等等,在C中并没有原生的数据结构来支持。因此对于这些对象的解析,我们统统会使用O这个占位符,转成PyObject *,然后通过宏Py_TYPE获取类型,然后再通过->tp_name,获取它到底是一个什么对象,是tuple啊、list啊、还是dict什么的。
还是先剧透一些api。
Py_TupleSize:获取python传递过来的元组内的元素个数
Py_TupleNew:接收一个整型,表示创建一个能容纳指定个数(PyObject *指针)的元组
PyTuple_Check:检测是否为一个PyTupleObject
Py_TupleSetItem:设置元素
Py_TupleGetItem:获取元素
下面我们来定义一个函数,接收一个只能存放int的元组,然后把里面的元素全部相加,然后返回。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
//创建一个元组,此时指针为NULL
PyObject *t = NULL;
//变量类型,要是通过先定义再赋值的方式,那么这里最好写const char *
//因为通过tp_name返回的也是一个const char *,尽管我们写成char *也没问题,但是编译会出警告,就让人不舒服
//这里type是一个指针,指向字符数组的首个元素的地址,这里表示指针指向的内容不能变,也就是对应的字符数组的首个元素不能变
//但是指针本身是可以变的,它想指向谁就指向谁,当然在做的各位C的水平肯定比我强,这里就不献丑了。
const char *type;
//元组的元素个数
int counts;
// 用于遍历的索引
int i;
//元组内的每一个元素
//都是一个PyObject *
PyObject *value;
//总和,如果传递的元组为空,那么总和为0
//python中sum(())或者sum([])也是这么做的
int sum = 0;
char *keys[] = {"t", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &t)){
return NULL;
}
//获取变量类型
type = Py_TYPE(t) -> tp_name;
//这是一种判断方式,还可以使用PyTuple_Check来检测传递过来的是不是一个元组
if (strcmp(type, "tuple") != 0){
PyErr_Format(PyExc_TypeError, "a tuple is required, but got %s", type);
return NULL;
}
//获取元素个数,还可以通过Py_SIZE(t) -> ob_size来获取,还记得吗?
//当时我们介绍了三个宏,Py_REFCNT、Py_TYPE、Py_SIZE,不记得了回头看看
//这里返回的类型其实是一个Py_ssize_t,我们使用int也是一样的
counts = PyTuple_Size(t);
for (i=0;i<counts;i++){
//获取对应元素,并检测是否为python中的int
value = PyTuple_GetItem(t, i);
if (!PyLong_Check(value)){
//不是int,则报错
PyErr_Format(PyExc_ValueError, "the value of index %d is not int, but %s", i, Py_TYPE(value) -> tp_name);
return NULL;
}
sum += PyLong_AsLong(value);
}
//结果返回
return PyLong_FromLong(sum);
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS,
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
try:
print(hanser.my_func1())
except Exception as e:
print(e) # Required argument 't' (pos 1) not found
try:
print(hanser.my_func1((1, 2, "", 4)))
except Exception as e:
print(e) # the value of index 2 is not int, but str
try:
print(hanser.my_func1([1, 2, 3, 4]))
except Exception as e:
print(e) # a tuple is required, but got list
try:
print(hanser.my_func1((1, 3, 5, 7))) # 16
except Exception as e:
print(e)
try:
print(hanser.my_func1(tuple(range(101)))) # 5050
except Exception as e:
print(e)
返回PyTupleObject
知道怎么传递了,那么下面我们返回一个元组。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
//这次不需要参数,但是我们的参数类型不用改
//这个类型是通用的,如果不接收参数,那么对应的元组或者字典就会空
//因此如果传入参数是不允许的话,那么你可以获取args和kw的ob_size进行检测一下
//如果不为零,那么返回异常并return NULL
//创建一个元组,容量为4
PyObject *t = PyTuple_New(4);
char *words[] = {"我永远", "喜欢", "satori", "酱~~"};
int i;
for (i=0;i<4;i++){
PyTuple_SetItem(t, i, PyUnicode_FromString(words[i]));
}
return t;
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS,
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
print(hanser.my_func1()) # ('我永远', '喜欢', 'satori', '酱~~')
PyListObject
传递和返回
PyListObject的传递和返回和PyTupleObject几乎是一样的,只不过PyListObject可以修改罢了,所以传递和返回我们就放在一起介绍了。
还是剧透几个api,和上面的PyTupleObject一样,只需要把Tuple换成List即可,我们这里就不写了。
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *l1 = NULL;
char *keys[] = {"l1", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &l1);
if (!PyList_Check(l1)){
PyErr_Format(PyExc_TypeError, "need a list, but got %s", Py_TYPE(l1) -> tp_name);
return NULL;
}
//拿到原来list对象的元素个数
Py_ssize_t counts = PyList_Size(l1);
//申请对应的空间
PyObject *l2 = PyList_New(counts);
//我们将list对象倒序返回
int i;
for (i=0; i<counts;i++){
//将l1的第i个元素,放在l2的counts-1-i的位置上
PyList_SetItem(l2, counts-1-i, PyList_GetItem(l1, i));
}
return l2;
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS, //参数类型要改成这个
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
print(hanser.my_func1([1, 2, "嘿嘿", "哈哈"])) # ['哈哈', '嘿嘿', 2, 1]
添加、插入、删除
我们说PyListObject对象是支持动态修改的,在python中可以进行append、insert、remove。那么它们对应的底层接口是什么呢,我们来看一下。
PyList_Append:在尾部添加一个元素
PyList_Insert:在中间插入一个元素
list_remove:删除指定的元素,但是我们发现这是小写的。因为python并没有给我们开放这个api,我们能使用的api都是大写的,基本上都是以PyXxx开头的。因此这个方法我们可以参考源码手动实现。
PyList_SetSlice:删除指定切片的元素
#include "Python.h"
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *l1 = NULL;
char *keys[] = {"l1", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &l1);
if (!PyList_Check(l1)){
PyErr_Format(PyExc_TypeError, "need a list, but got %s", Py_TYPE(l1) -> tp_name);
return NULL;
}
//假设传递有5个元素的list对象, [1, 2, 3, 4, 5]
//将第3个元素删除,那么直接将索引为2:3的部分设置为NULL即可,记得要转成PyObject *
PyList_SetSlice(l1, 2, 3, (PyObject *)NULL); // 变成[1, 2, 4, 5]
//再尾部添加一个python的字符串
PyList_Append(l1, PyUnicode_FromString("古明地觉")); // 变成[1, 2, 4, 5, "古明地觉"]
//在索引为1的地方插入一个float
PyList_Insert(l1, 1, PyFloat_FromDouble(3.14)); // 变成[1, 3.14, 2, 4, 5, "古明地觉"]
//删除值为5的元素
//以下是源码的实现方式,我们做了一点点修改
//因为list_remove这个方法没有开放给我们,在listobject.c中没有PyList_Remove这个方法
Py_ssize_t i;
for (i = 0; i < Py_SIZE(l1); i++) {
//循环循环遍历每一个元素,比较是否相等
//这个是富比较,接收两个值,以及一个操作(Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE)
//相等返回1,否则返回0
//注意:这里的l1是PyObject *,我们想调用ob_item的话,那么需要转成PyListObject *
//但是类型转换的优先级比,.xx、->xxx、[index]的优先级低,所以要加上小括号
int cmp = PyObject_RichCompareBool(((PyListObject *)l1)->ob_item[i], PyLong_FromLong(5), Py_EQ);
//cmp > 0,说明找到对应元素了,然后执行删除操作
if (cmp > 0) {
//实际上PyList_SetSlice底层调用的是list_ass_slice,而list_ass_slice并没有开放给我们使用
PyList_SetSlice(l1, i, i+1, (PyObject *)NULL);
//删除完毕之后跳出循环
break;
}
//我们看到这里居然没有{},这算是C语言的一个特点吧
//如果条件不满足,那么if条件的下面一行代码不会执行,注意只是下面的一行代码
//我们说cmp理论上要么为1、要么为0,如果小于0,说明富比较那里出错了
//比如:我们定义一个类A,然后重写__eq__方法,在里面raise一个异常,那么A的实例对象在比较的时候就会引发异常
else if (cmp < 0)
return NULL;
}
//如果i和Py_SIZE(l1)相等,证明走到头了,说明不存在指定的元素
if (i == Py_SIZE(l1)){
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
return NULL;
}
//操作执行完毕之后返回,因为list修改是在原地操作的
//所以这里返回一个None,注意不是NULL,因为返回NULL意味着报错了
//返回None,我们知道可以通过return Py_None,但是python还给我们提供了一个宏
//我们直接写Py_RETURN_NONE;表示return Py_None
Py_RETURN_NONE;
}
static PyMethodDef module_functions[] = {
{
"my_func1",
(PyCFunction)my_func1,
METH_VARARGS | METH_KEYWORDS, //参数类型要改成这个
"this is a function named my_func1",
},
{NULL, NULL}
};
static PyModuleDef HANSER = {
PyModuleDef_HEAD_INIT,
"hanser",
"this is a module named hanser",
-1,
module_functions,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_hanser(void)
{
return PyModule_Create(&HANSER);
}
import hanser
l = [1, 2, 3, 4, 5]
print(hanser.my_func1(l)) # None
print(l) # [1, 3.14, 2, 4, '古明地觉']
try:
hanser.my_func1([1, 2, 3])
except Exception as e:
print(e) # list.remove(x): x not in list
PyDictObject
下面我们来看看PyDictObject怎么在C中进行操作,老规矩还是来看看有哪些api。
解析字典:PyArg_ParseTuple(args, "O", &dic)
查看是不是PyDictObject:PyDict_Check(dic)
查看键值对个数:PyDict_Size(dic)
查看所有的key:PyDict_Keys(dic),会返回一个PyListObject *,里面是PyObject *
查看所有的value:PyDict_Values(dic),会返回一个PyListObject *,里面是PyObject *
查看所有的item:PyDict_Items(dic),会返回一个PyListObject *,里面是PyTupleObject *
迭代遍历:PyDict_Next(dic, Py_ssize_t *pos, PyObject **keys, PyObject **values)
查看字典是否包含某个key:PyDict_Contains(dic, key)
获取某个key对应的value:PyDict_GetItem(dic, key),key为PyObject *
获取某个key对应的value:PyDict_GetItemString(dic, key),key为char *,所以这个api只适用于key为str的类型的
设置key、value:PyDict_SetItem(dic, key, value)
删除一个key:PyDict_DelItem(dic, key),key不存在会抛异常
好吧,具体怎么操作就不演示了,可以自己尝试一下。
引用计数和内存管理
我们目前都没有涉及到内存管理的操作,我们说python中的对象都是申请在堆区的,这个是不会自动释放的。
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = PyUnicode_FromString("你好呀~~~");
return Py_None;
}
这个函数不需要参数,如果我们写一个死循环不停的调用这个函数,你会发现内存的占用蹭蹭的往上涨。就是因为这个PyUnicodeObject是申请在堆区的,此时内部的引用计数为1,尽管C中函数的变量存储在栈区,函数执行完毕变量s被销毁了,但是s是一个指针,这个指针被销毁了是不假,但是它指向的内存并没有被销毁。
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = PyUnicode_FromString("你好呀~~~");
Py_DECREF(s);
return Py_None;
}
因此我们需要手动调用Py_DECREF这个宏,来将s指向的PyUnicodeObject的引用计数减1,这样引用计数就为0了,至于到底是否被回收,就看我们在python中是否有变量去接收,如果有,那么引用计数会再次加1,于是就不会被回收。不过有一个特例,那就是当这个指针作为返回值的时候,我们不需要手动减去引用计数,因为会自动减。
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = PyUnicode_FromString("你好呀~~~");
//如果我们把s给返回了,那么我们就不需要调用Py_DECREF了
//因为一旦作为返回值,那么会自动减去1
//所以我们前面说C中的对象是由python来管理的,准确的说应该是作为返回值的指针指向的对象是由python来管理的
return s;
}
不过这里还存在一个问题,那就是我们在C中返回的是python传过来的
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = NULL;
char *keys[] = {"s", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
//传递过来一个PyObject *,然后原封不动的返回
//此时你会发现在python中,创建一个变量,然后传递到my_func1中
//再进行打印就会发生段错误,因为对应的内存已经被回收了
//如果能正常打印,说明在python中这个变量的引用计数不为1,可能是小整数对象池、或者有多个变量引用,那么就创建一个大整数或者其他的变量多调用几次。
//因为作为返回值,每次调用引用计数都会减1
//或者调用之前和调用之后分别使用sys.getrefcount函数查看引用计数的变化
return s;
}
因为s指向的内存不是在C中调用api创建的,而是python创建然后传递过来、解析出来的,也就是说这个s在解析之后已经指向了一块合法的内存。但是内存中的对象的引用计数是没有变化的,虽说有新的变量(这里的s)
指向它了,但是这个s是C中的变量不是python中的变量,因此你可以认为它的引用计数是没有变化的。
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
//假设创建一个PyListObject
PyObject *l1 = PyList_New(2);
//这个是将l1赋值给l2,但是不好意思,这两位老铁指向的PyListObject的引用计数还是1
PyObject *l2 = l1;
return s;
}
因此我们说,如果在C中创建一个PyObject的话,那么它的引用计数只会是1,因为对象被初始化了,引用计数默认是1。至于传递无论你在C中将创建PyObject返回的指针赋值给了多少个变量,它们指向的PyObject的引用计数都会是1。因为这些变量是C中的变量,不是python中的。
因此我们的问题就很好解释了,我们说当一个PyObject *作为返回值的时候,它指向的对象的引用计数会减去1,那么当python传递过来一个PyObject *指针的时候,由于它作为了返回值,因此引用计数会减1。因此当你在python中调用扩展函数结束之后,这个变量指向的内存可能就被销毁了。
如果你在python传递过来的指针没有作为返回值,那么怎么引用计数是不会发生变化的,但是一旦作为了返回值,引用计数会自动减1,因此我们需要手动的加1
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = NULL;
char *keys[] = {"s", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
//这样就没有问题了。
Py_INCREF(s);
return s;
}
因此我们可以得出如下结论:
如果在C中,创建一个PyObject *var,并且var已经指向了合法的内存,比如调用PyList_New、PyDict_New等等api返回的PyObject *,总之就是已经存在了PyObject,那么如果var没有作为返回值,我们必须手动地将var指向的对象的引用计数减1,否则这个对象就会在堆区一直待着不会被回收。可能有人问,如果PyObject *var2 = var,我将var再赋值给一个变量呢?那么只需要对一个变量进行Py_DECREF即可,当然对哪个变量都是一样的,因为在C中变量的传递不会导致引用计数的增加。
如果C中创建的PyObject *作为返回值而存在了,那么会自动将指向的对象的引用计数减1,因此此时该指针指向的内存就由python来管理了,就相当于在python中创建了一个对象,我们不需要关心。
最后关键的一点,如果C中返回的指针指向的内存是python中创建好的,假设我们在python中创建了一个对象,然后把指针传递过来了,但是我们说这不会导致引用计数的增加,因为赋值的变量是C中的变量。如果C中用来接收参数的指针没有作为返回值,那么引用计数在扩展函数调用之前是多少、调用之后还是多少。一旦作为了返回值,我们说引用计数会自动减1,因此假设你在调用扩展函数之前引用计数是3,那么调用之后你会发现引用计数变成了2。为了防止段错误,一旦作为返回值,我们需要在返回之前手动地将引用计数加1。
我们来看几个例子:
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = NULL;
char *keys[] = {"s", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
//假设这个s是一个PyDictObject *
if (!PyDict_Contains(s, PyUnicode_FromString("name"))){
//如果s不包含name这个key,终止函数,当然这里就不设置异常了
return NULL;
}
return Py_None;
}
如果写死循环,调用这个函数会怎么样?答案是内存的使用会不断增加,为什么?就是因为在检测的时候,我们写了PyUnicode_FromString("name"),这个对象是申请在堆区,我们并没有释放,因此每调用一次函数都会创建这样一个PyUnicodeObject对象,于是会导致内存使用不断增加。正确写法应该是这样:
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = NULL;
char *keys[] = {"s", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
PyObject *k = PyUnicode_FromString("name");
if (!PyDict_Contains(s, k)){
return NULL;
}
//先创建,然后再减去引用计数,这样就不会发生内存泄露了
Py_DECREF(k);
//可能有人问这个s怎么办?我们这个s指向的内存是python中创建的,它不会导致引用计数的变化
//而一旦函数结束,这个指针就被销毁了,因此指针也不会占用空间
//至于它指向的内存到底如何,就完全取决于python中的代码是怎么写的。
//因此我们python中的变量怎么过来的,就怎么回去,不会影响。
//前提是我们不能够返回这个s,而一旦返回了s,那么会自动的将s指向的对象的引用计数减1
//因此如果返回了s,我们还需要手动地调用Py_INCREF将s指向的对象的引用计数加1
//当然了,至于用C的类型创建的变量,像什么int啊、char *啊,这些就不用说了
//随着函数的结束会将指针连带指向的内存一块被销毁,我们关心的是PyObject *指向的内存,因为它是在堆区的
//栈区的变量我们不需要关心
return Py_None;
}
再比如:
static PyObject *
my_func1(PyObject *self, PyObject *args, PyObject *kw)
{
PyObject *s = NULL;
char *keys[] = {"s", NULL};
PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
PyObject *k = PyUnicode_FromString("name");
if (!PyDict_Contains(s, k)){
return NULL;
}
PyObject *v = PyDict_GetItem(s, k);
Py_DECREF(k);
return Py_None;
}
我们这里获取了字典s中键为k的value,并用v来接收。那么此时会不会发生内存泄露?答案是不会的,因为我们减去了k的引用计数。至于v,那么这个v指向的对象是谁创建的呢?显然是python中已经创建好的,因为获取的就是python中字典对应的值,如果我们再使用Py_DECREF(v);减去引用计数,那么反而有点"偷鸡不成蚀把米"的感觉,因为这有可能导致python中对应的value指向的内存被回收。
在不作为返回值的情况下:如果一个变量指向的内存是C中创建的,那么记得使用Py_DECREF将引用计数减1;如果是python中创建的,那么不需要做任何事情
在作为返回值的情况下:如果一个变量指向的内存是C中创建的,那么不需要做任何事情;如果是python中创建的,那么记得使用Py_INCREF将引用计数加1。
ok,就这么简单。
以上是关于如何在C语言中实现模块化的主要内容,如果未能解决你的问题,请参考以下文章
使用C语言为python编写动态模块--解析python中的对象如何在C语言中传递并返回