前言

首先注意:这里的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

pythone 异常未定义 python未定义名称_Python


完美。

放到pycharm里。

pythone 异常未定义 python未定义名称_Python_02


等等,这参数列表是怎么回事?

没有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,尝试调用里面的方法,发现可以显示参数名。

pythone 异常未定义 python未定义名称_python_03


故,我决定去python的源代码去寻找答案。结果是令人失望的。我们程序员开发python库利用的是python暴露给我们的api。而python源码内的库并没有利用api进行开发,而是更底层未封装的东西(也可能是用了,但我没能在几千个c文件内找到)。

搁浅1次。

2、尝试去搜度娘,stackflow等论坛,一无所获。
也难怪,这东西出在PyCharm身上。
搁浅2次。

3、尝试分析其他第三方库的源代码。

其他人写的第三方库总不可能不用python C/C++的api吧。这回就拿opencv来开刀。

pip安装opencv-python。

导入PyCharm

pythone 异常未定义 python未定义名称_python_04


很好,有参数名称。

去翻找源代码。

下载好后用cmake去config和generate,生成vs2019项目文件。具体参见我的另一篇关于编译opencv的文章:添加链接描述打开后,看着成百上千的cpp文件,手足无措。

笨方法,查找功能找PyArg_ParseTupleAndKeywords这个关键字。

pythone 异常未定义 python未定义名称_c++_05


很好,恰好这个cpp文件就叫cv2。

浏览了一下,这个文件是专门针对python版本的opencv封装,里面全是python的api编程。全都是python可调用的函数。

翻到最后,看看函数列表,模块初始化等等。

pythone 异常未定义 python未定义名称_Python_06


函数列表,函数还挺多的。参数2位置用了自定义的宏

pythone 异常未定义 python未定义名称_pythone 异常未定义_07


发现就是封装简化了一下我写的那个代码里的参数2和参数3。我也按照这种方式封装一下,发现问题并没有解决。

那么究竟是什么东西让PyCharm识别出来参数名呢?思考:

下面我说的关于c++的东西可能存在胡扯。

函数列表存了函数指针,也就是import后函数存到了内存里。PyCharm读到了这些内存,就形成了我们在PyCharm中看到的函数列表自动补全。

我们定义的变量a和b,是存在于函数代码块内。那么PyCharm能识别代码块码?肯定不能啊。编译好的pyd是个二进制文件,哪来的代码块。那opencv里,出现了参数名的又在哪里呢?

经过了一天的思考,对比,翻阅。我发现了。

在函数列表的帮助文档里!!

pythone 异常未定义 python未定义名称_python_08


如果PyCharm读到了文档,然后正则表达式处理一下。。。好像有道理。

解决

经过冥思苦想,与尝试。
方法如下:定义函数列表时,在函数的帮助文档内写上: 函数名(参数名…) -> 返回值类型

static PyMethodDef functionInit[] = {
	{"run", (PyCFunction)run, METH_VARARGS | METH_KEYWORDS,"run(a, b) -> None" },
	{NULL, NULL}
};