为什么要用C语言写Python模块,是Python不够香么?还是觉得头发还茂盛?都不是。因为C语言模块有几个显而易见的好处:
可以使用Python调用C标准库、系统调用等;
假设已经有了一堆C代码实现的功能,可以不用重写,岂不美滋滋;
性能?也算;
其他一些好处。
注:以下代码基于Python3。
开局举个栗
In a nutshell,用C编写Python模块就是下面几步:
准备工作
#include
// 没错,这就够了,什么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);
}
编译
编译也可以手动编译,只不过,懒。。。
from distutils.core import setup, Extension
module1 = 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表示该模块的名字,也就是在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 ", 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 ", 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
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 -fPIC
export PYTHONPATH=/home/example
Python中执行:
>>> import example
>>> example.say()
'Hello, World!'
>>>
瞬间感觉头发保住了。
等等,不是说用C吗?为什么最后乱入C++11?都差不多,who cares?
References
这就是我的底线!!欢迎搜索关注TensorBoy , 学习使我快乐!