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

    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 
    ", 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_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
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    【作者】张昺华
    【大饼教你学系列】https://edu.csdn.net/course/detail/10393
    【新浪微博】 张昺华--sky
    【twitter】 @sky2030_
    【微信公众号】 张昺华
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    计算机网络知识
    数据库知识
    操作系统知识
    计算机硬件基础知识
    计算机科学基础知识
    2019下半年软件设计师考试大纲
    软件设计师补题(2008下半年上午题)
    软件设计师补题(2008上半年上午题)
    测试复盘3
    测试复盘2
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/13624404.html
Copyright © 2020-2023  润新知