前言
首先注意:这里的c++开发Python库指的是调用Python C/C++ API,而不是在python里调用dll动态链接库。
最近在研究用c++编写Python库,一顿折腾。
网络上的教程给的例子都是定义一个只接受一个参数的函数,用c++来编写。
我爱折腾,写了一个需要2个参数的c++函数,来让Python调用。
这个模块名称我设置为pure_python,之所以这么命名,是之前研究过用c++的Boost库来开发Python库,现在想研究纯Python原生的开发方式。
工程的基本逻辑
C++函数逻辑很简单:
void run(int a, int b){
cout << "a:" << a << "\tb:" << "b" << endl;
}
就是输出a和b的值。
Python中调用:
import pure_python
# 方式1
pure_python.run(1, 2)
# 方式2
pure_python.run(b=2, a=1)
根据网上的教程,转换为Python能接受的开发方式:
#include "pch.h"
#include <Python.h>
#include <iostream>
static PyObject *run(PyObject *self, PyObject *args, PyObject *kwargs)
{
int a, b;
// 设置参数列表的名称,如果没有下面这行以及后面那段if语句,则python中不能通过pure_python.run(b=2, a=1)方式来调用
const char* kwlist[] = { "a", "b", NULL };
// 利用PyArg_ParseTupleAndKeywords这个API函数,来读取参数列表,把参数列表的值赋值给上面定义的变量
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii", kwlist, &a, &b))
// "ii"的意思是两个变量的类型为int,kwlist为参数名称数组
{
return NULL;
}
// 执行功能逻辑
cout << "a:" << a << "\tb:" << b << endl;
// 返回python中的None,即相当于啥也不返回
return Py_None;
}
// 定义函数表,也就是定义这个模块有哪些函数,是个结构体数组
// 结构体内{1, 2, 3, 4}。参数1为函数名称,参数2为函数指针,参数3为参数识别方式,参数4为帮助文档,即python用help()显示的内容
// 对于参数3,如果定义函数参数列表是(PyObject *self, PyObject *args, PyObject *kwargs)这样的,必须为METH_VARARGS | METH_KEYWORDS,如果参数列表是(PyObject *self, PyObject *args)这样的,必须为METH_VARARGS,关于其他识别方式,去看官方文档
static PyMethodDef functionInit[] = {
{"run", (PyCFunction)run, METH_VARARGS | METH_KEYWORDS,"run(a, b) -> None" },
{NULL, NULL}
};
// 定义模块信息
// 参数1:固定的PyModuleDef_HEAD_INIT,参数2:模块名,与项目名相同,与生成的dll文件相同;参数3:帮助文档。参数4:官方解释是允许的子解释器数量;参数4:函数列表
static PyModuleDef myModule = {
PyModuleDef_HEAD_INIT,
"pure_python",
"不使用BOOST的c++开发模块测试。\n内含有run方法",
-1,
functionInit
};
// 初始化模块
// python中import之时首先执行这个方法,相当于__init__.py文件
PyMODINIT_FUNC PyInit_pure_python(void)
{
return PyModule_Create(&myModule);
}
在vs2019中编译,生成的dll文件后缀名修改为pyd,放到python安装目录的DLLs文件夹下,运行python
完美。
放到pycharm里。
等等,这参数列表是怎么回事?
没有a和b这要是给别人用,那岂不是让人n脸懵逼。
之后把参数数量扩展到3个,发现,pycharm里仍然是args, kwargs
分析
python没有参数名的概念,在解释器内,通过解析函数来解析参数列表,解析方式有常用的两种:
1、元组:也就是纯按照位置来定参数,按0、1、2等等等等,分别赋值给定义好的变量,也就是PyArg_ParseTuple这个api
2、字典:按照名称来定参数。这个名称不是你在PyCharm中看到的参数名,也就是之前代码里的PyArg_ParseTupleAndKeywords这个api,我们传入a=1,b=2这对参数后,c++按照定义的kwlist数组内定义的参数名来进行匹配。
所以,我们得出结论:PyCharm中看到的参数名其实是PyCharm软件自身为程序员提供的便利,Python中并没有这个机制。
那么,PyCharm究竟是根据什么来确定出来的呢?
尝试解决
1、我尝试导入python自带的pyd的模块,例如pyexpat,尝试调用里面的方法,发现可以显示参数名。
故,我决定去python的源代码去寻找答案。结果是令人失望的。我们程序员开发python库利用的是python暴露给我们的api。而python源码内的库并没有利用api进行开发,而是更底层未封装的东西(也可能是用了,但我没能在几千个c文件内找到)。
搁浅1次。
2、尝试去搜度娘,stackflow等论坛,一无所获。
也难怪,这东西出在PyCharm身上。
搁浅2次。
3、尝试分析其他第三方库的源代码。
其他人写的第三方库总不可能不用python C/C++的api吧。这回就拿opencv来开刀。
pip安装opencv-python。
导入PyCharm
很好,有参数名称。
去翻找源代码。
下载好后用cmake去config和generate,生成vs2019项目文件。具体参见我的另一篇关于编译opencv的文章:添加链接描述打开后,看着成百上千的cpp文件,手足无措。
笨方法,查找功能找PyArg_ParseTupleAndKeywords这个关键字。
很好,恰好这个cpp文件就叫cv2。
浏览了一下,这个文件是专门针对python版本的opencv封装,里面全是python的api编程。全都是python可调用的函数。
翻到最后,看看函数列表,模块初始化等等。
函数列表,函数还挺多的。参数2位置用了自定义的宏
发现就是封装简化了一下我写的那个代码里的参数2和参数3。我也按照这种方式封装一下,发现问题并没有解决。
那么究竟是什么东西让PyCharm识别出来参数名呢?思考:
下面我说的关于c++的东西可能存在胡扯。
函数列表存了函数指针,也就是import后函数存到了内存里。PyCharm读到了这些内存,就形成了我们在PyCharm中看到的函数列表自动补全。
我们定义的变量a和b,是存在于函数代码块内。那么PyCharm能识别代码块码?肯定不能啊。编译好的pyd是个二进制文件,哪来的代码块。那opencv里,出现了参数名的又在哪里呢?
经过了一天的思考,对比,翻阅。我发现了。
在函数列表的帮助文档里!!
如果PyCharm读到了文档,然后正则表达式处理一下。。。好像有道理。
解决
经过冥思苦想,与尝试。
方法如下:定义函数列表时,在函数的帮助文档内写上: 函数名(参数名…) -> 返回值类型
static PyMethodDef functionInit[] = {
{"run", (PyCFunction)run, METH_VARARGS | METH_KEYWORDS,"run(a, b) -> None" },
{NULL, NULL}
};