Python进阶教程m7d–混合编程–代码级扩展

原文链接: http://www.juzicode.com/archives/1218

我们知道在Python中有文件、API、代码等多种层级的扩展。在 《Python进阶教程m7–混合编程–调用可执行文件》、《Python进阶教程m7b–混合编程–C语言接口ctypes(1) 》、《Python进阶教程m7c–混合编程–C语言接口ctypes(2) 》 等文章中对前面2种层级的扩展进行了介绍,这篇文章将介绍在代码级别进行扩展的方法,实践本文的代码需要对C语言及其编译有一定了解。

Python扩展的3个层级

在Python安装目录下include文件夹是头文件位置,libs文件夹是静态lib文件的位置,这2个路径需要添加到C语言工程的包含路径中。

1.新建工程文件-VS2015

这里选择VS2015为例,新建一个dll项目的工程:

从项目属性页检查是否为dll配置类型的工程:

添加Python头文件和lib文件目录,因为VS编译的是64bit的dll(pyd)文件,则需要选择64bit库文件的目录。这里D:\Python\Python38目录下安装的是64bit的Python解释器,在 D:\Python\Python38\libs目录下的lib文件就是64bit的静态库文件,如果安装了不同bit版本的解释器需要找到相应的路径添加。

添加附加依赖项python38.lib:

在生成事件中添加一条BAT命令,修改dll文件后缀为pyd,更符合Python使用习惯,同时自动会拷贝到你希望存放的位置:

2.构建pyd文件的步骤

我们先从一个最简单的例子入手,要生成的这个pyd文件只有一个函数。

2.1 step1、编写C/C++函数

先定义一个add函数,带2个int型入参,返回一个int型返回值,实现2个入参的加和并返回这个和:

#include <python.h>//包含python.h
int add(int a,int b)
{
	return a + b;
}

编写代码后就可以编译,检查是否生成了.pyd文件。

2.2 step2、封装函数

封装函数的模式类似这种形式:PyObject* wrapped_function(PyObject* self, PyObject* args),其中函数名称 wrapped_function 是封装后的函数名称,但是这个函数名称并不是最后要导出给Python调用的函数名称。

在这个函数内部实现封装方法,用到了2个重要的Python库函数,一个是PyAPI_FUNC(int) PyArg_ParseTuple(PyObject *, const char *, …),其中第1个参数是封装函数 wrapped_function 的入参 args ,第2个参数是一个const char*类型,在这个字符串中依次用单个字符声明要解析的 args 的类型,比如”id”的含义就是第1个“i”表示从 args 解析的第1个参数为int型,第2个“d”表示 从 args 解析的 第2个参数为double型,剩下的可变参数则按照 args 的入参顺序填入,用法类似于scanf()函数。另外一个是PyAPI_FUNC(PyObject *) Py_BuildValue(const char *, …),用来构建返回结果,参数的使用方法和 PyArg_ParseTuple 类似,从使用方法来看既然是可变参数说明构建的返回结果是可以有多个值的,这点和C函数是有差异的,C函数只允许有一个返回值,在这个例子中我们先看看最简单的只有1个返回值的情况。

PyObject* wrap_add_i(PyObject* self, PyObject* args)
{
	//定义入参、返回值变量
	int a,b, result;

	//解析参数,解析的结果赋值到变量a和b
	if (!PyArg_ParseTuple(args, "ii", &a,&b))//第2个字符串参数声明C语言函数的入参类型,第1个i表示被封装函数第1个入参为int型,依次类推
		return NULL;

	//调用被封装函数
	result = add(a,b);

	//返回结果
	return Py_BuildValue("i", result);//第1个字符串参数声明返回变量的类型,i表示int型
}

其实在这个例子中,也可以将add()函数内的语句写在wrap_add_i()函数内部实现,只不过这里将函数的实现add()和最后的封装 wrap_add_i()分隔开来,在函数库增大的时候更方便维护,封装做好接口后,后续的函数实现的改动并不需要修改封装部分的内容。

2.3 step3、构建PyMethodDef 数据

我们先看下PyMethodDef结构体的定义:

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

ml_name是导出函数的名称,这个才是最终给Python调用的函数名称,是字符串类型,ml_meth是被封装函数的函数名称,ml_flags是标志位,ml_doc是函数说明字符串可以为NULL。

这一步要定义一个PyMethodDef结构体数组,至少包含2部分,最后一部分是包含了4个NULL的PyMethodDef结构体,数组的其他元素包含一个或多个有实际意义的 PyMethodDef结构体,在这个例子中只有一个被封装函数,所以只需要写一个 PyMethodDef结构体数据。

static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
 
	{ NULL, NULL, NULL, NULL }
};

2.4 step4、构造PyModuleDef数据

PyModuleDef也是一个结构体数据,第2个成员变量是要导出的模块名称,字符串变量,第5个成员变量是一个PyMethodDef *指针,填入用前面第3步定义的 MethodsDef ,其他的变量可以用如下默认参数。

static PyModuleDef ModuleDef = {
	PyModuleDef_HEAD_INIT,
	"c2pyd",   //导出的模块名称
	NULL,      //模块文档,可以为NULL
	-1,       //一般为-1
	MethodsDef //PyMethodDef类型,导出方法
};

2.5 step5、定义 PyInit_xyz()函数

定义一个PyInit_xyz(void)函数,函数名称中的xyz必须和ModuleDef结构体的第2个成员名称一致,也就是要导出的模块名称。在该函数内部调用PyModule_Create()函数,入参为struct PyModuleDef*类型的变量,传入第4步定义的ModuleDef的地址。

PyMODINIT_FUNC
PyInit_c2pyd(void)
{
	return PyModule_Create(&ModuleDef);
}

经过上述5个步骤就可以完成一个模块的构造,编译完成后会生成一个pyd文件,该pyd文件名称必须要和第4步和第5步中的模块名称一致。 当需要增加函数时,步骤4,5不用做变化,重复上述的步骤1,2,并在步骤3的结构体数组中增加新函数列表即可。

3.使用模块

使用模块就和Python的原生模块一样,在Python代码中import这个生成的模块,模块的导入方法可以参考《Python进阶教程m1–模块(module)》。下面的例子就是在Python代码中调用add_i()函数,该函数名称在static PyMethodDef MethodsDef[]中定义。

import c2pyd  #不需要pyd后缀
print('c2pyd.add_i(1,5)=',c2pyd.add_i(1,5),'\n') #add_i在static PyMethodDef  MethodsDef[]中定义的
==========结果:
c2pyd.add_i(1,5)= 6

4.添加更多函数

有了前面的简单例程,接下来我们逐步修改程序,对C扩展功能进一步研究,这也是本教程提倡的一种学习方法,先有简单例子作为环境,有了基础后再纵向深入、横向扩展。

前面的例子我们实现了一个int类型的add()函数,下面开始添加float型的add函数,这时还可以重载使用add这个函数名称:

float add(float a, float b)
{
	return a + b;
}

从封装函数PyObject* wrap_add_i(PyObject* self, PyObject* args) 的定义可以看出,其入参和返回值都是固定的,当要封装一个float型的add()函数时,不能再使用 wrap_add_i 这个函数名称,可以增加一个 wrap_add_f 封装函数:

PyObject* wrap_add_f(PyObject* self, PyObject* args)
{
	float a, b, result;
	if (!PyArg_ParseTuple(args, "ff", &a, &b))//解析为2个float类型
		return NULL;
	result = add(a, b);
	return Py_BuildValue("f", result);//返回float型结果
}

然后添加结构体数据,导出函数名称为add_f:

static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },//添加
	{ NULL, NULL, NULL, NULL }
};

在Python代码中调用新增加的函数:

print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   
import c2pyd  #不需要pyd后缀
print('c2pyd.add_i(1,5)=',c2pyd.add_i(1,5),'\n') #add_i在static PyMethodDef  MethodsDef[]中定义的
print('c2pyd.add_f(1.1,5.5)=',c2pyd.add_f(1.1,5.5),'\n')
==========结果:
c2pyd.add_i(1,5)= 6 

c2pyd.add_f(1.1,5.5)= 6.599999904632568 

5.其他

5.1 PyMethodDef最后一个元素

在PyMethodDef MethodsDef[]中,最后一个元素为{ NULL, NULL, NULL, NULL },我们试着注释掉看会发生什么:

static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },
	//{ NULL, NULL, NULL, NULL }  //
};
print('-----欢迎来到www.juzicode.com')
print('-----公众号: 桔子code/juzicode \n')   

print('-----导入模块')
import c2pyd  #不需要pyd后缀
print('-----调用函数前')
print('c2pyd.add_i(1,5)=',c2pyd.add_i(1,5),'\n') #add_i在static PyMethodDef  MethodsDef[]中定义的
print('c2pyd.add_f(1.1,5.5)=',c2pyd.add_f(1.1,5.5),'\n')
print('-----调用函数后')

从上面的打印可以看出,在import语句时就发生了错误,但是没有任何提示信息,Python解释器就退出了。

实际上在PyMethodDef 的最后要求用NULL结束,是用来表示 PyMethodDef 数组结束的,PyModule_Create()函数在解析函数列表时需要这个结束标志位。

5.2 返回多个值

从Py_BuildValue()的函数声明可以看到,入参有可变参数,说明是支持返回多个变量的,这也和Python中函数可以返回一个多元素tuple是一致的,直接上例子:

PyObject* wrap_add_sub_f(PyObject* self, PyObject* args)
{
	float a, b, result, result2;
	if (!PyArg_ParseTuple(args, "ff", &a, &b))//解析为2个float类型
		return NULL;
	result = add(a, b);
	result2 = sub(a, b);
	return Py_BuildValue("ff", result, result2);//返回float型结果的和以及差值
}

static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },
	{ "add_sub_f",  wrap_add_sub_f,  METH_VARARGS, "Caculate two float" },
	{ NULL, NULL, NULL, NULL }
};
import c2pyd  #不需要pyd后缀

print('c2pyd.add_i(1,5)=',c2pyd.add_i(1,5),'\n') #add_i在static PyMethodDef  MethodsDef[]中定义的
print('c2pyd.add_f(1.1,5.5)=',c2pyd.add_f(1.1,5.5),'\n')
print('c2pyd.add_sub_f(1.1,5.5)=',c2pyd.add_sub_f(1.1,5.5),'\n')
==========结果:
c2pyd.add_i(1,5)= 6

c2pyd.add_f(1.1,5.5)= 6.599999904632568

c2pyd.add_sub_f(1.1,5.5)= (6.599999904632568, -4.400000095367432)

add_sub_f()函数在C代码的封装函数中返回的多个值,在Python的调用后返回的是一个tuple。

5.3 返回None

在Py_BuildValue()函数中只有一个空字符串,Python调用时将返回None:

PyObject* wrap_do_nothing(PyObject* self, PyObject* args)
{
	return Py_BuildValue("");//返回float型结果的和以及差值
}
static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },
	{ "add_sub_f",  wrap_add_sub_f,  METH_VARARGS, "Caculate two float" },
	{ "ret_nothing",  wrap_ret_nothing,  METH_VARARGS, "ret nothing" },
	{ NULL, NULL, NULL, NULL }
};
import c2pyd  #不需要pyd后缀

print('c2pyd.ret_nothing(1.1,5.5)=',c2pyd.ret_nothing(1.1,5.5),'\n')
==========结果:
c2pyd.ret_nothing(1.1,5.5)= None

5.4 没有入参

在定义函数列表的时候,METH_VARARGS用来定义有一个或多个变量的情形,当函数无入参时,则使用METH_NOARGS,比如下面例子中的{ “trans_noargs”, wrap_trans_noargs, METH_NOARGS, “transfer nothing” }。


PyObject* wrap_trans_noargs(PyObject* self, PyObject* args)
{
	printf("%s","wrap_trans_noargs: nothing\n");
	
	return Py_BuildValue("");//返回float型结果的和以及差值
}


static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },
	{ "add_sub_f",  wrap_add_sub_f,  METH_VARARGS, "Caculate two float" },
	{ "ret_nothing",  wrap_ret_nothing,  METH_VARARGS, "ret nothing" },
	{ "trans_noargs",  wrap_trans_noargs,  METH_NOARGS, "transfer noargs" },

	{ NULL, NULL, NULL, NULL }
};
import c2pyd
print('c2pyd.trans_noargs()=',c2pyd.trans_noargs(),'\n')
==========结果:
trans_noargs: nothing     #这里是c函数内部的打印内容
c2pyd.trans_noargs()= None

5.4 关键字入参

在Python函数中,入参可以采用关键字形式定义,特别是参数较多时用关键字入参代码的可读性更强,在C扩展中也支持使用关键字参数。

使用关键字入参时,封装函数的入参增加了一个PyObject *kw。在函数内部需要先定义一个字符串数组,该数组定义了从Python中传入的入参名称,并在最后填充一个NULL数据,入参名称是有顺序要求的,因为使用关键字参数也支持在调用函数的时候不声明参数名称,这个顺序就是不声明参数名称的顺序。解析参数不再使用PyArg_ParseTuple,而是使用PyArg_ParseTupleAndKeywords,第1个参数是封装函数的args,第2个参数是封装函数的kw,第3个参数是个字符串,由声明要解析的 args 的类型多个字符组成,第4个参数是定义了参数名称和参数顺序的关键字列表,剩余的可变参数就是要解析保存的变量。

在构建 PyMethodDef 数据时,封装函数名称前需要用(PyCFunction)进行类型转换,标志位使用METH_VARARGS| METH_KEYWORDS。

PyObject* wrap_trans_kwargs(PyObject* self, PyObject* args,PyObject *kw)
{
	char* kwlist[] = { "a","b","c","d", NULL };//最后一个元素必须为NULL,NULL前面的元素名称就是python调用传入的参数名称
	float a, b, c,d;
	if (!PyArg_ParseTupleAndKeywords( args, kw, "ffff", kwlist,  &a, &b, &c, &d))
		return NULL;
	printf("wrap_trans_kwargs: a=%f, b=%f, c=%f, d=%f\n", a,b,c,d);
	float result = add(a, b)+ add(c, d);

	return Py_BuildValue("f",result);//返回float型结果的和以及差值
}

static PyMethodDef  MethodsDef[] =
{
	//导出函数名称, 封装函数,参数模式,函数说明
	{ "add_i",  wrap_add_i,  METH_VARARGS, "Caculate two int"},
	{ "add_f",  wrap_add_f,  METH_VARARGS, "Caculate two float" },
	{ "add_sub_f",  wrap_add_sub_f,  METH_VARARGS, "Caculate two float" },
	{ "ret_nothing",  wrap_ret_nothing,  METH_VARARGS, "ret nothing" },
	{ "trans_noargs",  wrap_trans_noargs,  METH_NOARGS, "transfer  noargs" },
	{ "trans_kwargs",  (PyCFunction)wrap_trans_kwargs, METH_VARARGS| METH_KEYWORDS, "transfer kwargs" },

	{ NULL, NULL, NULL, NULL }
};
import c2pyd 
print('c2pyd.trans_kwargs(1.1,3.3,5.5,7.7)=',c2pyd.trans_kwargs(1.1,3.3,5.5,7.7),'\n') 
print('c2pyd.trans_kwargs(1.1,3.3,c=5.5,d=7.7)=',c2pyd.trans_kwargs(1.1,3.3,c=5.5,d=7.7),'\n') 
print('c2pyd.trans_kwargs(a=1.1,b=3.3,c=5.5,d=7.7)=',c2pyd.trans_kwargs(a=1.1,b=3.3,c=5.5,d=7.7),'\n') 
==========结果:
wrap_trans_kwargs: a=1.100000, b=3.300000, c=5.500000, d=7.700000
c2pyd.trans_kwargs(1.1,3.3,5.5,7.7)= 17.600000381469727

wrap_trans_kwargs: a=1.100000, b=3.300000, c=5.500000, d=7.700000
c2pyd.trans_kwargs(1.1,3.3,c=5.5,d=7.7)= 17.600000381469727

wrap_trans_kwargs: a=1.100000, b=3.300000, c=5.500000, d=7.700000
c2pyd.trans_kwargs(a=1.1,b=3.3,c=5.5,d=7.7)= 17.600000381469727

小结:这篇文章介绍了在Python中使用Python原生的C扩展功能封装C/C++代码的方法,先介绍了封装方法的5个基本步骤,再在此基础上介绍了None返回值,tuple返回值,无入参,关键字参数等内容。

通过Python混合编程系列教程,详细介绍了在文件、API、代码3个层级的封装和扩展方法。不同层级的封装对计算机语言的要求各不相同,API方式必须是C语言编写的动态链接库,要求最为严格,代码级别的封装则可以是C或者C++,文件级别理论上可以是任意语言编写的可执行文件,只要能在目标平台运行即可。可执行文件的方式只能等执行完了才能得到最终的结果,过程控制比较“笨拙”,API和代码级的扩展则能进行“精细”控制,自由度更高。但是使用内置C扩展功能的代码级扩展需要依赖特定的Python静态库完成编译,运行时也依赖特定的Python版本,而ctypes扩展则对Python版本无具体要求。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注