zoukankan      html  css  js  c++  java
  • 《深度剖析CPython解释器》27. 使用Python/C API编写扩展模块:编写扩展模块的整体流程

    楔子

    到目前为止,我们已经介绍了很多关于解释器方面的内容,本来接下来应该要说内存管理的,但是个人觉得应该对前面的系列做一个总结。而最好的方式,就是使用Python/C API编写扩展模块,个人是这么认为的。我们编写过不少Python的模块,但显然用的都是Python语言,那么问题来了,我们如何使用C语言来编写呢?

    使用C为Python编写扩展的话,是需要遵循一定套路的,而这个套路很固定。那么下面我们就来用Python写一个模块,看看这个模块如何使用C语言来实现。

    关于扩展模块,这里不得不提一下Cython,使用Python/C API编写扩展是一件不轻松的事情,其实还是C语言本身比较底层吧。而Cython则是帮我们解决了这一点,Cython代码和Python高度相似,而cython编译器会自动帮助我们将Cython代码翻译成C代码,再编译成扩展模块,所以Cython本质上也是使用了Python/C API,只不过它让我们不需要直接面对C,只要我们编写Cython代码即可,会自动帮我们转成C的代码。

    所以随着Cython的出现,现在使用Python/C API编写扩展的话算是越来越少了,不过话虽如此,使用Python/C API编写可以极大的帮助我们熟悉Python的底层。

    那么废话不多说,直接开始吧。

    编写扩展模块的基本骨架

    我们先用Python编写一个模块,模块名就叫fubuki吧,也就是创建一个fubuki.py。

    def f1():
        """return an integer"""
        return 123
    
    
    def f2(a):
        """return a + 123"""
        return a + 123
    

    非常简单的一个模块,里面只有两个简单的函数,但是我们知道当被导入时它就是一个PyModuleObject对象,里面除了我们定义的两个函数之外还有其它的属性,显然这是Python解释器在背后帮助我们完成的。

    那么我们如何使用C来进行编写呢?注意接下来,我的这波操作。

    /*
    编写Python扩展模块,需要引入Python.h这个头文件
    这个头文件,在Python安装目录的include目录下,编译的时候会自动寻找
    当然这个头文件里面还导入了很多其它的头文件,我们也可以直接拿来用
    */
    #include "Python.h"
    
    
    /*
    编写我们之前的两个函数f1和f2,必须返回PyObject *
    函数里面至少要接收一个PyObject *self,而这个参数我们是不需要管的,当然不叫self也是可以的
    显然跟方面方法里面的self是一个道理,所以对于Python调用者而言,f1是一个不需要接收参数的函数
    */
    static PyObject *
    f1(PyObject *self)
    {
        return PyLong_FromLong(123);
    }
    
    static PyObject *
    f2(PyObject *self, PyObject *a, PyObject *b)
    {
        long _a, _b;
        long result;
        _a = PyLong_AsLong(a);
        _b = PyLong_AsLong(b);
        result = _a + _b;
        return PyLong_FromLong(result);
    }
    
    /*
    定义一个结构体数组,结构体类型为PyMethodDef
    PyMethodDef里面有四个成员,分别是:函数名、函数指针(需要转成PyCFunction)、函数参数标识、函数的doc
    关于PyMethodDef我们后面会单独说
    */
    static PyMethodDef fubuki_function[] = {
        {
            "f1",
            (PyCFunction)f1,
            METH_NOARGS, //后面说
            "this is a function named f1"
        },
        {
            "f2",
            (PyCFunction)f2,
            METH_O, //后面说
            "this is a function named f2"
        },
    
        //结尾要有两个NULL
        {NULL, NULL}
    };
    
    
    /*
    我们编写的py文件,解释器会自动把它变成一个模块,但是这里我们需要手动定义
    定义一个PyModuleDef类型结构体
    */
    static PyModuleDef fubuki_module = {
        PyModuleDef_HEAD_INIT, //头部信息,Python.h中提供了这个宏
        "fubuki", //模块名
        "this a module named fubuki", //模块的doc,没有的话直接写成NULL即可
        -1, //模块的空间,这个不需要关心,直接写成-1即可
        fubuki_function, //上面的那么PyMethodDef结构数组,必须写在这里,不然我们没法使用定义的函数
        //下面直接写4个NULL即可
        NULL, NULL, NULL, NULL
    };
    
    
    //到目前为止,前置工作就做完了,下面还差两步
    /*
    扩展库入口函数,这是一个宏,Python的源代码我们知道是使用C来编写的
    但是编译的时候为了支持C++的编译器也能编译,于是需要通过extern "C"定义函数
    然后这样C++编译器在编译的的时候就会按照C的标准来编译函数,这个宏就是干这件事情的,主要和python中的函数保持一致
    */
    PyMODINIT_FUNC
    
    
    /*
    模块初始化入口,注意:模块名叫fubuki,那么下面就必须要写成PyInit_fubuki
    */
    PyInit_fubuki(void)
    {
        // 将使用PyModuleDef定义的模块对象的指针传递进去,然后返回得到Python中的模块
        return PyModule_Create(&fubuki_module);
    }
    

    整体逻辑还是非常简单的,过程如下:

    • include "Python.h",这个是必须的
    • 定义我们函数,具体定义什么函数、里面写什么代码完全取决于你的业务
    • 定义一个PyMethodDef结构体数组
    • 定义一个PyModuleDef结构体
    • 定义模块初始化入口,然后返回模块对象

    那么如何将这个C文件变成扩展模块,显然要经过编译。

    from distutils.core import *
    
    setup(
        # 打包之后会有一个egg_info,表示该模块的元信息信息,name就表示打包之后的egg文件名
        # 显然和模块名是一致的
        name="fubiki",
        version="1.11", # 版本号
        author="古明地盆",
        author_email="66666@东方地灵殿.com",
        # 关键来了,这里面接收一个类Extension,类里面传入两个参数
        # 第一个参数是我们的模块名,必须和PyInit_xxx中的xxx保持一致,否则报错
        # 第二个参数是一个列表,表示用到了哪些C文件,因为扩展模块对应的C文件不一定只有一个,我们这里是fubuki.c
        ext_modules=[Extension("fubuki", ["fubuki.c"])]
    )
    

    当前的py文件名叫做1.py,我们在控制台中直接输入python 1.py install即可。

    我们看到对应的pyd已经生成了,自动帮我们拷贝到site-packages目录中了。

    我们看到了一个fubuki.pyd文件,至于中间的部分就是解释器版本,然后上面的egg-info,指的就是模块fubuki的元信息,我们打开看看。

    有几个我们没有写,所以是UNKNOW,当然这都不重要,重要的是我们能不能调用了。试一试吧,当然要先把之前写的fubuki.py删掉。

    import fubuki
    
    print(fubuki.f1())  # 123
    print(fubuki.f2(123))  # 246
    

    最后再看一个神奇的东西,我们知道在pycharm这样的智能编辑器中,通过Ctrl加左键可以调到指定模块的指定位置。

    神奇的一幕出现了,我们点击进去居然还能跳转,其实我们在编译成扩展模块移动到site-packages之后,pycharm就自动生成了。我们看到模块注释、函数的注释跟我们在C文件中指定的一样。

    函数参数(位置参数)

    编写扩展模块的基本骨架按照上面说的那样子做就可以,不过我们还遗留了一个问题,那就是参数。我们看到上面必须定义一个PyMethodDef结构体数组,那么这个PyMethodDef是什么呢?我们之前说过的。

    struct PyMethodDef {
        /* 内置的函数或者方法名 */
        const char  *ml_name;   
        /* 实现对应逻辑的C函数,但是需要转成PyCFunction类型,主要是为了更好的处理关键字参数 */
        PyCFunction ml_meth;    
        
        /* 参数类型 
        #define METH_VARARGS  0x0001  扩展位置参数
        #define METH_KEYWORDS 0x0002  扩展关键字参数
        #define METH_NOARGS   0x0004  不需要参数
        #define METH_O        0x0008  需要一个参数
        #define METH_CLASS    0x0010  被classmethod装饰
        #define METH_STATIC   0x0020  被staticmethod装饰   
        */
        int         ml_flags;   
        
        //函数的__dic__
        const char  *ml_doc; 
    };
    typedef struct PyMethodDef PyMethodDef;
    

    如果不需要参数,那么ml_flags传入一个METH_NOARGS,接收一个参数传入METHOD_O即可,所以我们上面的f1对应的ml_flags是METHOD_NOARGS,f2对应的ml_flags是METHOD_O。

    如果是多个参数,那么直接写成METHOD_VARAGRS即可,也就是通过扩展位置参数的方式,但是这要如何解析呢?比如:有一个函数f3接收3个参数,这在C中要如何实现呢?

    static PyObject *
    f3(PyObject *self, PyObject *args)
    {
        //目前我们定义了一个PyObject *args,如果是METH_O,那么这个args就是对应的一个参数
        //如果METH_VARAGRS,还是只需要定义一个*args即可,只不过此时的*args是一个PyTupleObject,我们需要将多个参数解析出来
        //假设此时我们这个函数是接收3个int,然后相加    
        int a, b, c;
        /*
        下面我们需要使用PyArg_ParseTuple进行解析,因为我们接收三个参数
        这个函数返回一个整型,如果失败会返回0,成功返回非0
        */
        if (!PyArg_ParseTuple(args, "iii", &a, &b)){
            //失败返回NULL,后面我们会介绍如何在底层返回一个异常
            return NULL;
        }
        return PyLong_FromLong(a + b + c);
    }
    

    所以重点就在PyArg_ParseTuple上面,这个函数的原型就是:int PyArg_ParseTuple(PyObject *args, const char *format, ...),我们注意到format,显然类似于printf,里面肯定是一些占位符,那么都支持哪些占位符呢?

    • i:接收一个Python中的int,然后解析成C的int
    • l:接收一个Python中的int,然后将传来的值解析成C的long
    • f:接收一个Python中的float,然后将传来的值解析成C的float
    • d:接收一个Python中的float,然后将传来的值解析成C的double
    • s:接收一个Python中的str,然后将传来的值解析成C的char *
    • u:接收一个Python中的str,然后将传来的值解析成C的wchar_t *
    • O:接收一个Python中的object,然后将传来的值解析成C的PyObject *

    占位符比较多,但是我们暂时只需要掌握上面七个即可,下面演示一下。

    #include "Python.h"
    
    static PyObject *
    f(PyObject *self, PyObject *args)
    {
        int a;
        long b;
        float c;
        double d; //关于object和字符串我们暂时先不试,留在后面,目前只需要知道这个函数怎么用即可
        if (!PyArg_ParseTuple(args, "ilfd", &a, &b, &c, &d)){
            return NULL;
        }
        printf("int: %d, long: %d, float: %f, double: %lf
    ", a, b, c, d);
        return Py_None; //Py_None就是Python中的None
    }
    
    
    static PyMethodDef fubuki_function[] = {
        {
            "f",
            (PyCFunction)f,
            METH_VARARGS, //这里记得一定要改成METH_VARARGS,表示接收多个参数
            "this is a function named f"
        },
    
        {NULL, NULL}
    };
    
    
    static PyModuleDef fubuki_module = {
        PyModuleDef_HEAD_INIT,
        "fubuki",
        "this a module named fubuki",
        -1,
        fubuki_function,
        NULL, NULL, NULL, NULL
    };
    
    
    PyMODINIT_FUNC
    
    PyInit_fubuki(void)
    {
        return PyModule_Create(&fubuki_module);
    }
    

    然后编译成扩展模块,执行一下吧。当然这里编译的过程我们就不显示了,跟之前是一样的。并且为了方便,我们的模块名就不改了,还叫fubuki,但是编译之后的pyd文件内容已经变了,不过需要注意的是,编译之后会有一个build目录,然后会自动把里面的pyd文件拷贝到site-packages中,如果你修改了代码,但是模块名没有变的话,那么编译之后的文件名还和原来一样。如果一样的话,那么它发现已经存在相同文件了,就不会再拷贝了。因此两种做法:要么你把模块名给改了,这样编译会生成新的模块。要么编译之前记得把上一次编译生成的build目录先删掉,我们推荐第二种做法,不然site-packages目录下会出现一大堆我们自己定义的模块。

    import fubuki
    
    # 传参不符合,自动给你报错
    try:
        print(fubuki.f())
    except TypeError as e:
        print(e)  # function takes exactly 4 arguments (0 given)
    
    try:
        print(fubuki.f(123))
    except TypeError as e:
        print(e)  # function takes exactly 4 arguments (1 given)
    
    try:
        print(fubuki.f(123, "xxx", 123, 123))
    except TypeError as e:
        print(e)  # an integer is required (got type str)
    
    fubuki.f(123, 123, 123, 123)  # int: 123, long: 123, float: 123.000000, double: 123.000000
    

    怎么样,是不是很简单呢?当然PyArg_ParseTuple解析失败,Python底层自动帮你报错了,告诉你缺了几个参数,或者类型是哪里错了。

    设置异常

    下面我们介绍一下如何设置一个异常,我们还是定义一个函数,接收3个object对象。第一个参数类型是int、第二个参数类型是float、第三个参数类型是str。

    #include "Python.h"
    
    static PyObject *
    f(PyObject *self, PyObject *args)
    {
        PyObject PyObject *obj1, *obj2, *obj3;
        /*
        Py_REFCNT: 获取引用计数
        Py_TYPE: 获取类型
        Py_SIZE: 获取ob_size
        */
        int ob_size = Py_SIZE(args);
        if (ob_size != 3){
            //设置完异常之后直接return NULL即可,PyErr_Format用来设置异常,接收异常类型(PyExc_异常)、异常信息
            PyErr_Format(PyExc_TypeError, "function f need takes 3 argument, but got %d", ob_size);
            return NULL;
        }
    
        //得到三个PyObject *
        if (!PyArg_ParseTuple(args, "OOO", &obj1, &obj2, &obj3)){
            return NULL;
        }
        int arg1 = strcmp(Py_TYPE(obj1) -> tp_name, "int") == 0;
        if (!arg1){
            PyErr_Format(PyExc_TypeError, "arg1 require an integer, got %s", Py_TYPE(obj1) -> tp_name);
            return NULL;
        }
        
        int arg2 = strcmp(Py_TYPE(obj2) -> tp_name, "float") == 0;
        if (!arg2){
            PyErr_Format(PyExc_TypeError, "arg2 require a float, got %s", Py_TYPE(obj2) -> tp_name);
            return NULL;
        }
        
        int arg3 = strcmp(Py_TYPE(obj3) -> tp_name, "str") == 0;
        if (!arg3){
            PyErr_Format(PyExc_TypeError, "arg3 require an string, got %s", Py_TYPE(obj3) -> tp_name);
            return NULL;
        }
        
        return PyUnicode_FromString("congratulations~~~"); 
    }
    
    static PyMethodDef fubuki_function[] = {
        {
            "f",
            (PyCFunction)f,
            METH_VARARGS, //这里记得一定要改成METH_VARARGS
            "this is a function named f"
        },
    
        {NULL, NULL}
    };
    
    static PyModuleDef fubuki_module = {
        PyModuleDef_HEAD_INIT,
        "fubuki",
        "this a module named fubuki",
        -1,
        fubuki_function,
        NULL, NULL, NULL, NULL
    };
    
    PyMODINIT_FUNC
    
    PyInit_fubuki(void)
    {
        return PyModule_Create(&fubuki_module);
    }
    

    编译成扩展模块,调用一下。

    import fubuki
    
    # 传参不符合,自动给你报错
    try:
        print(fubuki.f())
    except TypeError as e:
        print(e)  # function f need takes 3 argument, but got 0
    
    try:
        print(fubuki.f(123))
    except TypeError as e:
        print(e)  # function f need takes 3 argument, but got 1
    
    try:
        print(fubuki.f(123, "xxx", 123, 123))
    except TypeError as e:
        print(e)  # function f need takes 3 argument, but got 4
    
    try:
        print(fubuki.f(123, 123, "xxx"))
    except TypeError as e:
        print(e)  # arg2 require a float, got int
    
    print(fubuki.f(123, 123., "xxx"))  # congratulations~~~
    

    传递关键字参数

    传递关键字参数的话,那么我们可以通过key=value的方式来实现,那么在C中我们如何解析呢?既然支持关键字的方式,那么是不是也可以实现默认的参数,就是我们不传会使用默认值呢?答案是支持的,我们知道解析位置参数是通过PyArg_ParseTuple,那么解析关键字参数是通过PyArg_ParseTupleAndKeywords

    //函数原型
    int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...) 
    

    我们看到相比原来的PyArg_ParseTuple,多了一个kw和一个char *类型的数组,具体怎么用我们在编写代码的时候说。

    #include "Python.h"
    
    static PyObject *
    f(PyObject *self, PyObject *args, PyObject *kw)
    {
        //我们说函数既可以通过位置参数、还可以通过关键字参数传递,那么函数的参数类型就要变成METH_VARARGS | METH_KEYWORDS
        //假设我们定义了三个参数,name、age、place,这三个参数可以通过位置参数传递、也可以通过关键字参数传递
        //这里我们先只使用纯英文,等到介绍到字符串的Python/C API的时候再说
        char *name;
        int age = -1;
        char *where = "japan";
    
        //告诉python解释器,参数的名字,注意:这里面字符串的顺序就是函数定义的参数顺序
        //也是keys后面的变量顺序,其实变量名字叫什么无所谓,但是类型要和format中对应的占位符匹配,只是为了一致我们会起相同的名字
        //注意结尾要有一个NULL,否则会报出段错误。
        char *keys[] = {"name", "age", "place", NULL}; //这个NULL很重要
    
        //解析参数,我们看到format中本来应该是sis的,但是中间出现了一个|
        //这就表示|后面的参数是可以不填的,如果不填会使用我们上面给出的默认值
        //因此这里name就是必填的,因为它在|的前面,而age和where可以不填,如果不填就用我们上面给出的默认值
        //keys就是定义的参数的名字,后面把参数的指针传进去
        if (!PyArg_ParseTupleAndKeywords(args, kw, "s|is", keys, &name, &age, &where)){
    		return NULL;
    	}
        printf("name: %s, age: %d, place: %s
    ", name, age, where);
        return Py_None;
    }
    
    
    static PyMethodDef fubuki_function[] = {
        {
            "f",
            (PyCFunction)f,
            METH_VARARGS | METH_KEYWORDS,
            "this is a function named f"
        },
    
        {NULL, NULL}
    };
    
    
    static PyModuleDef fubuki_module = {
        PyModuleDef_HEAD_INIT,
        "fubuki",
        "this a module named fubuki",
        -1,
        fubuki_function,
        NULL, NULL, NULL, NULL
    };
    
    
    PyMODINIT_FUNC
    
    PyInit_fubuki(void)
    {
        return PyModule_Create(&fubuki_module);
    }
    

    老规矩,还是编译成扩展模块。

    import fubuki
    
    try:
        fubuki.f()
    except TypeError as e:
        print(e)  # function missing required argument 'name' (pos 1)
    
    fubuki.f("mea")  # name: mea, age: -1, place: japan
    fubuki.f("mea", 38)  # name: mea, age: 38, place: japan
    fubuki.f("mea", 38, "JAPAN")  # name: mea, age: 38, place: JAPAN
    

    以上我们便实现了支持关键字参数的函数,我们说使用PyArg_ParseTupleAndKeywords的时候,里面写上了args和kw,这表示既支持位置参数、也支持关键字参数。至于顺序就是代码中的顺序,我们看到如果参数传递的个数不正确,Python会自动提示你。

    返回布尔类型和None

    我们说函数都必须返回一个PyObject *,如果这个函数没有返回值,那么在Python中实际上返回的是一个None,但是我们不能返回NULL,None和NULL是两码事。在扩展函数中,如果返回NULL就表示这个函数执行的时候,不符合某个逻辑,我们需要终止掉,不能再执行下去了。这是在底层,但是在Python的层面,你需要告诉使用者为什么不能执行了,或者说底层的哪一行代码不满足条件,因此这个时候我们会在return NULL之前需要手动设置一个异常,这样在Python代码中就会报错,才知道为什么底层函数退出了。当然有时候会自动帮我们设置,比如上面的参数解析错误。

    但是返回None的话,Python也提供了一个宏,我们直接写Py_RETURN_NONE;即可,表示返回一个None,当然也可以使用之前的return Py_None。如果是bool类型的话,那么就是return Py_True;return Py_False;,比较简单,就不代码演示了。

    引用计数和内存管理

    下面来说一个非常关键的地方,就是引用计数和内存管理。我们目前都没有涉及到内存管理的操作,但是我们说Python中的对象都是申请在堆区的,这个是不会自动释放的。

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
        PyObject *s = PyUnicode_FromString("你好呀~~~");
        return Py_None;	
    }
    

    这个函数不需要参数,如果我们写一个死循环不停的调用这个函数,你会发现内存的占用蹭蹭的往上涨。就是因为这个PyUnicodeObject是申请在堆区的,此时内部的引用计数为1。函数执行完毕变量s被销毁了,但是s是一个指针,这个指针被销毁了是不假,但是它指向的内存并没有被销毁。

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
    	
        PyObject *s = PyUnicode_FromString("你好呀~~~");
        Py_DECREF(s);
        return Py_None;	
    }
    

    因此我们需要手动调用Py_DECREF这个宏,来将s指向的PyUnicodeObject的引用计数减1,这样引用计数就为0了。不过有一个特例,那就是当这个指针作为返回值的时候,我们不需要手动减去引用计数,因为会自动减。

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
        PyObject *s = PyUnicode_FromString("你好呀~~~");
        //如果我们把s给返回了,那么我们就不需要调用Py_DECREF了
        //因为一旦作为返回值,那么会自动减去1
        //所以C中的对象是由python来管理的,准确的说应该是作为返回值的指针指向的对象是由python来管理的
        return s;	
    }
    

    不过这里还存在一个问题,那就是我们在C中返回的是Python传过来的

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
    	
    	PyObject *s = NULL;
    	char *keys[] = {"s", NULL};
    	PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
    	
    	//传递过来一个PyObject *,然后原封不动的返回
    	return s;	
    }
    

    显然上面s指向的内存不是在C中调用api创建的,而是Python创建然后传递过来、解析出来的,也就是说这个s在解析之后已经指向了一块合法的内存。但是内存中的对象的引用计数是没有变化的,虽说有新的变量(这里的s)指向它了,但是这个s是C中的变量不是Python中的变量,因此你可以认为它的引用计数是没有变化的。然后作为返回值返回之后,指向对象的引用计数减一。所以你会发现在Python中,创建一个变量,然后传递到my_func1中,执行完之后再进行打印就会发生段错误,因为对应的内存已经被回收了。如果能正常打印,说明在Python中这个变量的引用计数不为1,可能是小整数对象池、或者有多个变量引用,那么就创建一个大整数或者其他的变量多调用几次,因为作为返回值,每次调用引用计数都会减1。

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
        //假设创建一个PyListObject
        PyObject *l1 = PyList_New(2);
        //将l1赋值给l2,但是不好意思,这两位老铁指向的PyListObject的引用计数还是1
        PyObject *l2 = l1;
        return s;	
    }
    

    因此我们说,如果在C中创建一个PyObject的话,那么它的引用计数只会是1,因为对象被初始化了,引用计数默认是1。至于传递,无论你在C中将创建PyObject返回的指针赋值给了多少个变量,它们指向的PyObject的引用计数都会是1。因为这些变量是C中的变量,不是Python中的。

    因此我们的问题就很好解释了,我们说当一个PyObject *作为返回值的时候,它指向的对象的引用计数会减去1,那么当Python传递过来一个PyObject *指针的时候,由于它作为了返回值,因此引用计数会减1。因此当你在Python中调用扩展函数结束之后,这个变量指向的内存可能就被销毁了。如果你在Python传递过来的指针没有作为返回值,那么怎么引用计数是不会发生变化的,但是一旦作为了返回值,引用计数会自动减1,因此我们需要手动的加1。

    static PyObject *
    my_func1(PyObject *self, PyObject *args, PyObject *kw)
    {	
    	
        PyObject *s = NULL;
        char *keys[] = {"s", NULL};
        PyArg_ParseTupleAndKeywords(args, kw, "O", keys, &s);
        //这样就没有问题了。
        Py_INCREF(s);
        return s;	
    }
    

    因此我们可以得出如下结论:

    • 如果在C中,创建一个PyObject *var,并且var已经指向了合法的内存,比如调用PyList_New、PyDict_New等等api返回的PyObject *,总之就是已经存在了PyObject,那么如果var没有作为返回值,我们必须手动地将var指向的对象的引用计数减1,否则这个对象就会在堆区一直待着不会被回收。可能有人问,如果PyObject *var2 = var,我将var再赋值给一个变量呢?那么只需要对一个变量进行Py_DECREF即可,当然对哪个变量都是一样的,因为在C中变量的传递不会导致引用计数的增加。
    • 如果C中创建的PyObject *作为返回值而存在了,那么会自动将指向的对象的引用计数减1,因此此时该指针指向的内存就由Python来管理了,就相当于在Python中创建了一个对象,我们不需要关心。
    • 最后关键的一点,如果C中返回的指针指向的内存是Python中创建好的,假设我们在python中创建了一个对象,然后把指针传递过来了,但是我们说这不会导致引用计数的增加,因为赋值的变量是C中的变量。如果C中用来接收参数的指针没有作为返回值,那么引用计数在扩展函数调用之前是多少、调用之后还是多少。一旦作为了返回值,我们说引用计数会自动减1,因此假设你在调用扩展函数之前引用计数是3,那么调用之后你会发现引用计数变成了2。为了防止段错误,一旦作为返回值,我们需要在返回之前手动地将引用计数加1。

    C中创建的:不作为返回值,引用计数手动减1、作为返回值,不处理;Python中创建传递过来的,不作为返回值,不处理、作为返回值,引用计数手动加1。

    另外关于Py_INCREF和Py_DECREF,它们要求PyObject *不可以为NULL,如果可能为NULL的话,那么建议使用Py_XINCREF和Py_XDECREF。当然虽然我们演示使用的是Py_INCREF和Py_DECREF,但是我们建议实际项目中都使用Py_XINCREF和Py_XDECREF。

    小结

    这次我们介绍了Python编写扩展模块的基本骨架,至于一些类型对象的API,前面已经剖析过了,所以一些API的调用可以自己去源码中查找,或者查阅官网。只不过最重要的是,我们需要先了解编写扩展所对应的流程是什么,以及函数的位置参数、关键字参数、设置异常、引用计数等等,这些都是贯穿始终的,只有先掌握这些,才能继续往下学习。

    后续我们将介绍Python的内存管理。

  • 相关阅读:
    用Latex编辑数学公式
    《利用Python进行数据分析》学习笔记之Matplotlib : pandas中的绘图函数
    《利用Python进行数据分析》学习笔记之Pandas基础
    splice()使用
    markdown语法
    【Docker】Docker 镜像
    【Docker】Docker 命令
    【Docker】Docker 安装
    【Docker】Docker 简介
    【MySQL 高级】知识拓展
  • 原文地址:https://www.cnblogs.com/traditional/p/13670333.html
Copyright © 2011-2022 走看看