PY08-06:Python的类扩展


  实际C++的类是很难直接导出为Python的类,这缘于C++在编译的时候有一个过程就是修饰命名。但是在C中导出Python还是可以的,这个就是Python提供的C接口来实现(CPython)。这个主题就是实现一个C扩展模块提供一个Python类扩展。


C++扩展类实现

基本的头文件

#include <iostream>
#include <sstream>
#include <Python.h>
#include <structmember.h>
  1. iostream用户标准IO输入输出

    • 这里只使用标准输出:std::cout
  2. sstream是字符串处理/内存处理

    • 字符串拷贝
  3. Python的C API接口实现

    • 大部分API都是这个头文件一共声明。
  4. structmember定义了结构体的类型定义

    • T_STRING/T_INT等
    • PyMemberDef数据成员的定义

数据成员

数据成员定义

  • 使用结构体定义数据,这一点不奇怪。实施Python的函数,类都是用C扩展实现。实际上没有C++什么事儿,就算我们人为使用了部分C++语法,但是从底层本质来说还是是C的实现最方便。
typedef struct _Sobel{
    PyObject_HEAD 
    char *m_filename;
    int   m_fd;
} Sobel;

数据成员描述

  • 数据成员描述,使用的是PyMemberDef类型数组,PyMemberDef这是一个结构体,其成员是数据成员需要描述的5个信息。
typedef struct PyMemberDef {
    char *name;           // 成员名
    int type;             // 类型
    Py_ssize_t offset;    // 偏离位置
    int flags;            // 属性修饰标识READONLY , READ_RESTRICTED, PY_WRITE_RESTRICTED,RESTRICTED
    char *doc;           // 成员的doc文档描述
} PyMemberDef;
  • 描述上面定义的数据如下
    • 最后空的一个记录,表示结束。

static PyMemberDef  m_data[] = {
    {"m_filename", T_STRING, offsetof(Sobel, m_filename), 0, "bmp file name"},
    {"m_fd",       T_INT,    offsetof(Sobel, m_fd),       0, "file descriptor"},
    {NULL,         NULL,     NULL,                        0, NULL}
};

数据成员初始化

  • 核心还是参数分析,与返回值处理,但是初始化没有返回值。
    • 下面的逻辑是通过命名参数filename找到参数名,并初始化成员。
static void Sobel_init(Sobel *self, PyObject*args, PyObject*kwargs){
    const char* filename;
    static char *argsname[] = {"filename", NULL};
    
    if(!PyArg_ParseTupleAndKeywords(args, kwargs, "s", argsname, &filename)){
        std::cout << "parse init parameter error!" << std::endl;
        return;
    }
    self->m_filename = new char[strlen(filename) + 1];
    errno_t er = strcpy_s(self->m_filename, strlen(filename) + 1, filename);
    std::cout << "init ok:" << self->m_filename << std::endl;
}

数据成员的释放

  • 释放是主要是释放成员的内存空间
static void Sobel_destruct(Sobel *self){
    if(self->m_filename){
        delete[] self->m_filename;
    }
}

函数成员

函数成员定义

  • 函数与构造器与虚构器的差别是不管有返回值没有,都有PyObject指针返回。
    • 这里实现了open与rotate(简单实现)
      • open模拟打开文件
      • rotate模拟旋转图像
static PyObject* Sobel_open(Sobel* self){
    std::cout << "Open file ok" << std::endl;
    return Py_BuildValue("s", self->m_filename);;
}

static PyObject *Sobel_rotate(Sobel *self, PyObject *args){
    float  angle;
    if(!PyArg_ParseTuple(args, "f", &angle)){
        return Py_None;
    }
    std::cout << "rotate OK:" << angle << std::endl;
    return Py_None; 
}

函数成员描述

  • 描述函数使用PyMethodDef结构体,多个函数使用PyMethodDef数组来描述。
struct PyMethodDef {
    const char  *ml_name;   /* Python中调用的函数名*/
    PyCFunction ml_meth;    /* C++实现的函数指针,只要类型转换,三种类型的函数(keywords参数函数,元组参数的函数,没有参数的函数) */
    int         ml_flags;   /* 标记用来指定函数的参数的三种方式:METH_VARARGS, METH_KEYWORDS, METH_NOARGS */
    const char  *ml_doc;    /* 函数的doc文档 */
};
typedef struct PyMethodDef PyMethodDef;
  • 上面open与rotate函数的描述
static PyMethodDef  m_functions[] = {
    {"open",   (PyCFunction)Sobel_open,   METH_NOARGS,  "open image file of bmp format"},
    {"rotate", (PyCFunction)Sobel_rotate, METH_VARARGS, "rotate image"},
    {NULL, NULL, NULL, NULL}
};

模块初始化的工作

  • 模块的初始化只要是创建模块,并绑定一个Python的类型(元类对象:元类就是类型的类型,实例化后就是我们一般意义上的类)

模块描述

  • 注意:这类的函数不能在模块中描述,在模块中描述的函数与数据,导出为Python的全局函数与数据。

  • 模块描述采用PyModuleDef结构体,PyModuleDef结构体定义如下:

typedef struct PyModuleDef{
  PyModuleDef_Base m_base;    // 模块的开始地址。一般使用固定的宏PyModuleDef_HEAD_INIT
  const char* m_name;         // 模块名(安装的时候需要使用的名字)
  const char* m_doc;          // 模块的文档
  Py_ssize_t m_size;          // 模块的大小,一般使用-1表示自动确定
  PyMethodDef *m_methods;     // 全局函数
  struct PyModuleDef_Slot* m_slots;     
  traverseproc m_traverse;
  inquiry m_clear;
  freefunc m_free;
} PyModuleDef;

#ifdef __cplusplus
}
  • 我们创建的模块描述如下:
static PyModuleDef  def_module = {
    PyModuleDef_HEAD_INIT,
    "sobel",
    "This is doc for Class Sobel!",
    -1,
    NULL,    // 我们的函数没有在这儿绑定
    NULL,
    NULL,
    NULL,
    NULL
};

类型对象描述(Python类)

  • Python的类是元类的对象,所以这儿描述的是元类对象。元类对象的描述也是使用C结构体, 这个结构体太彪悍,彪悍的人生不要解释,但是可以通过可阅读的成员名,可以知道其表示的含义。
typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;

#ifdef COUNT_ALLOCS
    /* these must be last and never explicitly initialized */
    Py_ssize_t tp_allocs;
    Py_ssize_t tp_frees;
    Py_ssize_t tp_maxalloc;
    struct _typeobject *tp_prev;
    struct _typeobject *tp_next;
#endif
} PyTypeObject;
  • 我们实现的Python类描述如下:
static PyTypeObject classinfo = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "This is doc for Module", 
    sizeof(Sobel),
    0,
    (destructor)Sobel_destruct,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    "This is class for Sobel!",
    0,
    0,
    0,
    0,
    0,
    0,
    m_functions, 
    m_data,
    0,
    0,
    0,
    0,
    0,
    0,
    (initproc)Sobel_init, 
    0,
    PyType_GenericNew,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0
};
  • 注意:其中PyVarObject_HEAD_INIT(NULL, 0)的含义也是确定头地址的。
#define PyObject_HEAD                   PyObject ob_base;

#define PyObject_HEAD_INIT(type)        \
    { _PyObject_EXTRA_INIT              \
    1, type },

#define PyVarObject_HEAD_INIT(type, size)       \
    { PyObject_HEAD_INIT(type) size },

模块初始化函数定义

  • 这个函数按照命名预定的:PyInit_模块名
PyMODINIT_FUNC PyInit_sobel(void){
    PyObject* mod;
    // 1. 创建模块
    // 2. 添加类型到导出的模块
    return mod;
}

创建模块

  • 创建模块使用PyModule_Create函数,函数原型如下
PyAPI_FUNC(PyObject *) PyModule_Create2(struct PyModuleDef*, int apiver);
#define PyModule_Create(module)  PyModule_Create2(module, PYTHON_API_VERSION)
#define PYTHON_API_VERSION 1013
    mod = PyModule_Create(&def_module);
    if(mod == 0){
        return Py_None;
    }
    Py_INCREF(&def_module);
  • 创建成功,增加一次引用计数。

添加类型对象到模块(Python类)

  • 添加模块使用函数PyModule_AddObject实现,函数定义如下:
PyAPI_FUNC(int) PyModule_AddObject(PyObject *, const char *, PyObject *);
  • 在添加Python类之前,建议检测下是否描述完备,这个函数是PyType_Ready
PyAPI_FUNC(int) PyType_Ready(PyTypeObject *);
  • 我们把我们描述的类直接加入即可:
    if(PyType_Ready(&classinfo) < 0){
        return Py_None;
    }
    PyModule_AddObject(mod, "Sobel", (PyObject*)&classinfo);

完整的模块导出的初始化工作实现

PyMODINIT_FUNC PyInit_sobel(void){
    PyObject* mod;
    if(PyType_Ready(&classinfo) < 0){
        return Py_None;
    }
    mod = PyModule_Create(&def_module);
    if(mod == 0){
        return Py_None;
    }
    Py_INCREF(&def_module);
    PyModule_AddObject(mod, "Sobel", (PyObject*)&classinfo);
    return mod;
}

编译与测试

编译脚本setup.py

from distutils.core import setup, Extension
setup(
    name="=sobel",
    version="1.0",
    ext_modules=[
        Extension("sobel", sources=["sobel.cpp"], language="C++"), 
    ]
)

编译命令

  • 命令:

    • python setup.py build_ext --inplace
  • 编译过程截图

编译过程
  • 编译后生成动态库文件:
    • sobel.cp36-win_amd64.pyd

测试Python程序test.py

from sobel import *

# help(Sobel)
s = Sobel("gpu.bmp")
print("调用返回:",s.open())
s.rotate(45)

  • 执行后结果如下:
在Python中执行的结果
  • 这么繁琐的C扩展,我想很多人内心是顽皮马儿跑过,所以还有一个东西可以考虑学学Cython。实际Cython的性能是否真比C高,这个估计需要评估下,使用C是王道啊。

  • 导出的文档帮助如下

from sobel import *

help(Sobel)
Help on class This is doc for Module in module builtins:

class This is doc for Module(object)
 |  This is class for Sobel!
 |  
 |  Methods defined here:
 |  
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  open(...)
 |      open image file of bmp format
 |  
 |  rotate(...)
 |      rotate image
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  m_fd
 |      file descriptor
 |  
 |  m_filename
 |      bmp file name

附录:

说明

  • 本主题的内容还是没有实现C++的类被导出到Python,仅仅是利用C实现了一个Python类而已。

    • 要导出C++类还需要二次调用。
  • 官方的文档地址如下:

    • https://docs.python.org/dev/c-api/index.html

完整的代码:sobel.cpp

  • 这个代码使用C也可以一样实现,使用C++是我们估计的,这个里面的额核心语法都是C。
#include <iostream>
#include <sstream>
#include <Python.h>
#include <structmember.h>

typedef struct _Sobel{
    PyObject_HEAD 
    char *m_filename;
    int   m_fd;
} Sobel;


static PyMemberDef  m_data[] = {
    {"m_filename", T_STRING, offsetof(Sobel, m_filename), 0, "bmp file name"},
    {"m_fd",       T_INT,    offsetof(Sobel, m_fd),       0, "file descriptor"},
    {NULL,         NULL,     NULL,                        0, NULL}
};

static void Sobel_init(Sobel *self, PyObject*args, PyObject*kwargs){
    const char* filename;
    static char *argsname[] = {"filename", NULL};
    
    if(!PyArg_ParseTupleAndKeywords(args, kwargs, "s", argsname, &filename)){
        std::cout << "parse init parameter error!" << std::endl;
        return;
    }
    self->m_filename = new char[strlen(filename) + 1];
    errno_t er = strcpy_s(self->m_filename, strlen(filename) + 1, filename);
    std::cout << "init ok:" << self->m_filename << std::endl;
}

static void Sobel_destruct(Sobel *self){
    if(self->m_filename){
        delete[] self->m_filename;
    }
}

static PyObject* Sobel_open(Sobel* self){
    std::cout << "Open file ok" << std::endl;
    return Py_BuildValue("s", self->m_filename);;
}

static PyObject *Sobel_rotate(Sobel *self, PyObject *args){
    float  angle;
    if(!PyArg_ParseTuple(args, "f", &angle)){
        return Py_None;
    }
    std::cout << "rotate OK:" << angle << std::endl;
    return Py_None; 
}

static PyMethodDef  m_functions[] = {
    {"open",   (PyCFunction)Sobel_open,   METH_NOARGS,  "open image file of bmp format"},
    {"rotate", (PyCFunction)Sobel_rotate, METH_VARARGS, "rotate image"},
    {NULL, NULL, NULL, NULL}
};

static PyTypeObject classinfo = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "This is doc for Module", 
    sizeof(Sobel),
    0,
    (destructor)Sobel_destruct,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    "This is class for Sobel!",
    0,
    0,
    0,
    0,
    0,
    0,
    m_functions, 
    m_data,
    0,
    0,
    0,
    0,
    0,
    0,
    (initproc)Sobel_init, 
    0,
    PyType_GenericNew,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0
};

static PyModuleDef  def_module = {
    PyModuleDef_HEAD_INIT,
    "sobel",
    "This is doc for Class Sobel!",
    -1,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL
};

PyMODINIT_FUNC PyInit_sobel(void){
    PyObject* mod;
    if(PyType_Ready(&classinfo) < 0){
        return Py_None;
    }
    mod = PyModule_Create(&def_module);
    if(mod == 0){
        return Py_None;
    }
    Py_INCREF(&def_module);
    PyModule_AddObject(mod, "Sobel", (PyObject*)&classinfo);
    return mod;
}


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269