使用C语言编写Python模块-引子【转】

转自:https://www.jianshu.com/p/47590edc355c

为什么要用C语言写Python模块,是Python不够香么?还是觉得头发还茂盛?都不是。因为C语言模块有几个显而易见的好处:

  1. 可以使用Python调用C标准库、系统调用等;
  2. 假设已经有了一堆C代码实现的功能,可以不用重写,岂不美滋滋;
  3. 性能?也算;
  4. 其他一些好处。

注:以下代码基于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);
}
编译

编译也可以手动编译,只不过,懒。。。

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 = '[email protected]',
       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扩展模块步骤基本就是上面提到的这几个步骤就可以完成(重复啰嗦):

  1. 定义你需要暴露给CPython解析器的函数;
  2. 用一个PyMethodDef结构体列表去给出所有需要暴露的函数的元数据,对第一步中所定义的函数进行映射以及说明,让解析器知道文怎去构造一个Python调用;
  3. 用一个PyModuleDef去给出此模块的元数据;
  4. 给出一个当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_nameml_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 dummym_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 -fPIC
export 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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章