使用C语言编写Python模块-引子【转】
为什么要用C语言写Python模块,是Python不够香么?还是觉得头发还茂盛?都不是。因为C语言模块有几个显而易见的好处:
- 可以使用Python调用C标准库、系统调用等;
- 假设已经有了一堆C代码实现的功能,可以不用重写,岂不美滋滋;
- 性能?也算;
- 其他一些好处。
注:以下代码基于Python3。
开局举个栗
In a nutshell,用C编写Python模块就是下面几步:
准备工作
#include<Python.h>// 没错,这就够了,什么stdio.h就都有了
定义API
static PyObject* say_hello(PyObject* self, PyObject* args) { printf("Hello world, I just a demo.");
}
注册API
// PyMethodDef 是一个结构体static PyMethodDef my_methods[] = {
{ "say", say_hello, 0, "Just show a greeting." },
{NULL, NULL, 0, NULL}
};
注册模块
static struct PyModuleDef my_module = { PyModuleDef_HEAD_INIT,
"dummy",
NULL,
-1,
my_methods
};
初始化
PyMODINIT_FUNC PyInit_mymodule(void) { return PyModule_Create(&my_module);
}
编译
编译也可以手动编译,只不过,懒。。。
python">from distutils.core import setup, Extensionmodule1 = Extension(\'dummy\',
define_macros = [(\'MAJOR_VERSION\', \'1\'),
(\'MINOR_VERSION\', \'0\')],
sources = [\'my_module.c\'])
setup (name = \'DummyModule\',
version = \'1.0\',
description = \'This is a demo package\',
author = \'zmyzhou\',
author_email = \'no@email.here.com\',
url = \'https://docs.python.org/extending/building\',
long_description = \'\'\'This is really just a demo package.\'\'\',
ext_modules = [module1]
)
运行
export PYTHONPATH=/home/example(misc) $ python
Python 3.5.2 (default, Oct 8 2019, 13:06:37)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dummy
>>> dummy.say()
Hello world, I just a demo.
>>>
解剖麻雀啦
总得来说,想用C写Python扩展模块步骤基本就是上面提到的这几个步骤就可以完成(重复啰嗦):
- 定义你需要暴露给CPython解析器的函数;
- 用一个
PyMethodDef
结构体列表去给出所有需要暴露的函数的元数据,对第一步中所定义的函数进行映射以及说明,让解析器知道文怎去构造一个Python调用; - 用一个
PyModuleDef
去给出此模块的元数据; - 给出一个当Python解释器加载该模块时候的构造函数
PyInit_<Module_name>
, 其中Module_name
表示该模块的名字,也就是在PyModuleDef
中给出的模块名,例子中是dummy
,那么这个函数名最后就是PyInit_dummy
。
虽然说简洁是智慧的精华,但是也太简单了,裤子都脱了,你就给我看这个?
少侠且慢动手,容我解释解释。
API 需要符合什么要求?
由于在Python语言中,在几乎所有场景中对类型时不加以区分的,而C语言是区分类型的,那怎么办?解决办法是只用一种C类型表示,而这个类型就是PyObject
。而这个PyObject
到底是什么可以暂且不管,就好似总说五百年前是一家,究竟五百年前这家户主是谁,我们很多时候没必要知道。
此外,由于几乎多有Python对象对生存在堆上,因此我们接口中的对象(变量)也应该生存在堆上,所以我们用指针来索引,即PyObject*
。到此,我们的函数原型呼之欲出。
在Python中我们定义一个函数时这样子:
def func(*args): # do something here
那么我们C中定义的函数也类似:
PyObject* func(PyObject* self, PyObject* args) { // I too do something here
}
是不是似曾相识?如果这个函数是个模块函数,那么self
表示NULL
或者一个特定指向的指针,如果是类中的方法,self
就表示为当前调用该方法的实例;args
就表示参数列表。比如,我们觉得上面例子中``say_hello`总是复读机式输出同一句话太单调,我们现在想让他鹦鹉学舌,我们可以改成:
PyObject* echo(PyObject* self, PyObject* args) { const char* what;
PyArg_ParseTuple(args, "s", &what);
printf("Python said: %s", what);
return Py_None;
}
输出为:
>>> import dummy
>>> dummy.echo(\'Hello there!\')
Python said: Hello there!
>>>
上面echo
的例子中我们发现了一个奇怪的东西混了进来:PyArg_ParseTuple
。这是什么?我说是魔法肯定被打。
输入参数和返回处理
输入
上面说过,Python中我们很少关心某个变量是什么类型,我们用PyObject
表示所有从Python传过来的值类型,但是由于C语言是强类型语言,只用一种类型是没办法正常工作的。因此我们需要把这种类型变成C语言中相应的类型。就好似古代夜观天象,每天都可以出现流星,但是一般人也看不懂天象啊,这只能让星官来解释,星官根据不同现象来解释,是大吉大利还是不详。PyArg_ParseTuple
就是做这个翻译的工作,其函数声明如下:
int PyArg_ParseTuple(PyObject *args, const char* format, ...);
其中args
就是API中的args
参数,format
就是你要将args
中的对应参数翻译成C语言中的什么类型。例如上面echo
的例子中,我们就将其翻译成了char*
字符串。通过format="s"
来指示PyArg_ParseTuple
我们传入的args
第一个参数是字符串。如果我们还想多几个参数,那么怎么办?好办。我们使用format="si"
来表示我们第一个参数是字符串,第二个参数是整型。
PyObject* echo(PyObject* self, PyObject* args) { const char* what;
int count;
PyArg_ParseTuple(args, "si", &what, &count);
int i = 0;
for(; i < count; i++)
printf("Python said: %s \n", what);
return Py_None;
}
这样我们的输出就变成了:
>>> import dummy>>> dummy.echo(\'repeat my word 3 times.\', 3)
Python said: repeat my word 3 times.
Python said: repeat my word 3 times.
Python said: repeat my word 3 times.
>>>
更多关于如何解析Python穿过来的参数的方法以及如何使用相对应的format
,请参阅这里。
返回
来而不往非礼也。有传进来的,那就肯定有传出去的。事情完成没完成都应该对请求的人有个交代。那我们怎么把特定的C类型变量丢还给Python呢?使用Py_BuildValue
,其实就是类似于PyArg_ParseTuple
反过来。我们例子中返回来Python中的None
,我们也可以返回一句话。例如:
PyObject* echo(PyObject* self, PyObject* args) { const char* what;
int count;
char* feedback = "Job is done.";
PyArg_ParseTuple(args, "si", &what, &count);
int i = 0;
for(; i < count; i++)
printf("Python said: %s \n", what);
return Py_BuildValue("s", feedback);
}
>>> fb = dummy.echo(\'Repeat my word 4 time and give me feedback.\', 4)Python said: Repeat my word 4 time and give me feedback.
Python said: Repeat my word 4 time and give me feedback.
Python said: Repeat my word 4 time and give me feedback.
Python said: Repeat my word 4 time and give me feedback.
>>> print(fb)
Job is done.
>>>
更多细节请参阅这里
怎么注册API?
注册API,需要用到一个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_flags
,它控制着Python怎样把参数传过来,我上面例子中用到的一直是METH_VARARGS
这也是一种比较常用的标志,它表示我们所注册的API接收两个参数,一个self
用于表示调用者本身,另一个args
表示个tuple
。还有其他几种标志可选。另外注意区分ml_name
和ml_meth
,前者表示在Python中调用时的名字,后者表示在C语言中定义的方法名字。详情请看这里。
怎么注册模块?
与注册API类似,注册模块也用到一个结构体PyModuleDef
,其定义如下:
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;
traverseproc m_traverse;
inquiry m_clear;
freefunc m_free;
}PyModuleDef;
怎么看着比我们例子中的多了很多项?其实多出来的我们只需要特别关心m_name, m_doc, m_size, m_methods
这四项。第一项PyModuleDef_Base
的值肯定是PyModuleDef_HEAD_INIT
,这是个宏,具体是啥我们不需要管。
要注意的是,n_name
就是将来你在Python中导入该模块时的名字,比如这里我们设置n_name="dummy"
,我们在使用的时候就是import dummy
;m_doc
就是我们使用dummy.__doc__
将输出的内容,属于对模块的说明,例如:
static struct PyModuleDef my_module = { PyModuleDef_HEAD_INIT,
"dummy",
"Sometimes NO DOC is the best DOC.",
-1,
my_methods
};
则输出为:
>>> import dummy>>> print(dummy.__doc__)
Sometimes NO DOC is the best DOC.
m_methods
就是上面注册的API。详情看这里。
The end? Not yet.
另外还有个很重要的概念就是引用计数,这个一时半会也说不清,这篇文章的目的本来就是抛砖引玉,大概了解用C语言开发Python模块是个什么流程,我们的目的也达到了。
很繁琐,我一个写Python、三行代码就可以为所欲为的人,怎么忍受得了这些花里胡哨?幸运的是,所有程序员的痛是一样的,大家都不喜欢繁琐,大家都追求的是简洁。因此诞生了Boost.python这种库,之后由于Boost太庞大,又出现了类似功能的轻量级pybind11。例如使用pybind11
,下面代码个就可以完成我们上面繁琐的工作:
#include<pybind11/pybind11.h>namespace py = pybind11;
char* greet() {
return "Hello, World!";
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example module";
// Add bindings here
m.def("say", greet);
}
然后用一下命令编译并设置PYTHONPATH:
c++ -O3 -Wall -shared -std=c++11 -I/home/example/playground/pybind11/include my_module.c -o example.so -I/usr/include/python3.5m -I//home/example/playground/pybind11/include -fPICexport PYTHONPATH=/home/example
Python中执行:
>>> import example>>> example.say()
\'Hello, World!\'
>>>
瞬间感觉头发保住了。
等等,不是说用C吗?为什么最后乱入C++11?都差不多,who cares?
References
https://docs.python.org/3.7/extending/extending.html#the-module-s-method-table-and-initialization-function
https://docs.python.org/3/c-api/index.html
https://www.python.org/dev/peps/pep-0007/
https://github.com/pybind/pybind11
这就是我的底线!!欢迎搜索关注TensorBoy , 学习使我快乐!
作者:SunnyZhou1024
链接:https://www.jianshu.com/p/47590edc355c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以上是 使用C语言编写Python模块-引子【转】 的全部内容, 来源链接: utcz.com/z/386519.html