C 확장을 사용하여 Python에 특정 기능을 제공합니다.
이전 글에서는 6가지 Python 인터프리터를 소개했습니다. CPython은 대부분의 시스템에서 기본 인터프리터이며, 여론 조사에 따르면 가장 인기 있는 인터프리터입니다. Cpython의 고유한 기능은 확장 API를 사용하여 C로 Python 모듈을 작성하는 기능입니다. C로 Python 모듈을 작성하면 Python의 사용 편의성을 유지하면서 계산 집약적인 코드를 C로 이동할 수 있습니다.
이 글에서는 C++ 확장 모듈을 작성하는 방법을 보여드리겠습니다. 대부분의 컴파일러는 일반적으로 두 언어를 모두 이해하므로 C 대신 C++를 사용합니다. 나는 단점을 미리 언급해야 합니다: 이런 방식으로 구축된 Python 모듈은 다른 인터프리터로 이식 가능하지 않습니다. CPython 인터프리터에서만 작동합니다. 따라서 C 언어 모듈과 상호 작용하는 보다 이식성 있는 방법을 찾고 있다면 ctypes 모듈 사용을 고려해 보세요.
언제나 그렇듯이 GitHub에서 관련 소스 코드를 찾을 수 있습니다. 웨어하우스에 있는 C++ 파일의 용도는 다음과 같습니다:
my_py_module.cpp
: Python 모듈MyModule
정의my_py_module.cpp
: Python 模块MyModule
的定义my_cpp_class.h
: 一个头文件 - 只有一个暴露给 Python 的 C++ 类my_class_py_type.h/cpp
: Python 形式的 C++ 类pydbg.cpp
: 用于调试的单独应用程序本文构建的 Python 模块不会有任何实际用途,但它是一个很好的示例。
在查看源代码之前,你可以检查它是否能在你的系统上编译。我使用 CMake 来创建构建的配置信息,因此你的系统上必须安装 CMake。为了配置和构建这个模块,可以让 Python 去执行这个过程:
$ python3 setup.py build
或者手动执行:
$ cmake -B build$ cmake --build build
之后,在 /build
子目录下你会有一个名为 MyModule. so
的文件。
首先,看一下 my_py_module.cpp
文件,尤其是 PyInit_MyModule
函数:
PyMODINIT_FUNCPyInit_MyModule(void) {PyObject* module = PyModule_Create(&my_module);PyObject *myclass = PyType_FromSpec(&spec_myclass);if (myclass == NULL){return NULL;}Py_INCREF(myclass);if(PyModule_AddObject(module, "MyClass", myclass) < 0){Py_DECREF(myclass);Py_DECREF(module);return NULL;}return module;}
这是本例中最重要的代码,因为它是 CPython 的入口点。一般来说,当一个 Python C 扩展被编译并作为共享对象二进制文件提供时,CPython 会在同名二进制文件中(<ModuleName>.so
)搜索 PyInit_<ModuleName>
my_cpp_class.h
: 하나의 헤더 파일 - Python에 노출되는 항목 클래스
my_class_py_type.h/cpp
: Python 형식의 C++ 클래스🎜pydbg.cpp
: 디버깅을 위한 별도의 애플리케이션🎜이 문서 작성 Python 모듈은 실제로 사용되지는 않지만 좋은 예입니다. 🎜🎜Building Modules🎜🎜소스 코드를 보기 전에 시스템에서 컴파일되는지 확인할 수 있습니다. 저는 CMake🎜를 사용하여 빌드 구성 정보를 생성하므로 시스템에 CMake를 설치해야 합니다. 이 모듈을 구성하고 빌드하려면 Python이 이 프로세스를 수행하도록 할 수 있습니다: 🎜static PyType_Spec spec_myclass = {"MyClass",// namesizeof(MyClassObject) + sizeof(MyClass),// basicsize0,// itemsizePy_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flagsMyClass_slots // slots};
typedef struct {PyObject_HEADint m_value;MyClass*m_myclass;} MyClassObject;
/build
하위 디렉토리에 MyModule.so
파일 . 🎜🎜확장 모듈 정의🎜🎜먼저 살펴보세요 my_py_module.cpp
파일, 특히 PyInit_MyModule
함수: 🎜static PyType_Slot MyClass_slots[] = {{Py_tp_new, (void*)MyClass_new},{Py_tp_init,(void*)MyClass_init},{Py_tp_dealloc, (void*)MyClass_Dealloc},{Py_tp_members, MyClass_members},{Py_tp_methods, MyClass_methods},{0, 0} /* Sentinel */};
<ModuleName>.so
) 검색 PyInit_<ModuleName>
함수 가져올 때 실행하세요. 🎜无论是声明还是实例,所有 Python 类型都是 PyObject 的一个指针。在此函数的第一部分中,module
通过 PyModule_Create(...)
创建的。正如你在 module
详述(my_py_module
,同名文件)中看到的,它没有任何特殊的功能。
之后,调用 PyType_FromSpec 为自定义类型 MyClass
创建一个 Python 堆类型 定义。一个堆类型对应于一个 Python 类,然后将它赋值给 MyModule
模块。
注意,如果其中一个函数返回失败,则必须减少以前创建的复制对象的引用计数,以便解释器删除它们。
MyClass
详述在 my_class_py_type.h 中可以找到,它作为 PyType_Spec 的一个实例:
static PyType_Spec spec_myclass = {"MyClass",// namesizeof(MyClassObject) + sizeof(MyClass),// basicsize0,// itemsizePy_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flagsMyClass_slots // slots};
它定义了一些基本类型信息,它的大小包括 Python 表示的大小(MyClassObject
)和普通 C++ 类的大小(MyClass
)。MyClassObject
定义如下:
typedef struct {PyObject_HEADint m_value;MyClass*m_myclass;} MyClassObject;
Python 表示的话就是 PyObject 类型,由 PyObject_HEAD
宏和其他一些成员定义。成员 m_value
视为普通类成员,而成员 m_myclass
只能在 C++ 代码内部访问。
PyType_Slot 定义了一些其他功能:
static PyType_Slot MyClass_slots[] = {{Py_tp_new, (void*)MyClass_new},{Py_tp_init,(void*)MyClass_init},{Py_tp_dealloc, (void*)MyClass_Dealloc},{Py_tp_members, MyClass_members},{Py_tp_methods, MyClass_methods},{0, 0} /* Sentinel */};
在这里,设置了一些初始化和析构函数的跳转,还有普通的类方法和成员,还可以设置其他功能,如分配初始属性字典,但这是可选的。这些定义通常以一个哨兵结束,包含 NULL
值。
要完成类型详述,还包括下面的方法和成员表:
static PyMethodDef MyClass_methods[] = {{"addOne", (PyCFunction)MyClass_addOne, METH_NOARGS,PyDoc_STR("Return an incrmented integer")},{NULL, NULL} /* Sentinel */};static struct PyMemberDef MyClass_members[] = {{"value", T_INT, offsetof(MyClassObject, m_value)},{NULL} /* Sentinel */};
在方法表中,定义了 Python 方法 addOne
,它指向相关的 C++ 函数 MyClass_addOne
。它充当了一个包装器,它在 C++ 类中调用 addOne()
方法。
在成员表中,只有一个为演示目的而定义的成员。不幸的是,在 PyMemberDef 中使用的 offsetof 不允许添加 C++ 类型到 MyClassObject
。如果你试图放置一些 C++ 类型的容器(如 std::optional),编译器会抱怨一些内存布局相关的警告。
MyClass_new
方法只为 MyClassObject
提供一些初始值,并为其类型分配内存:
PyObject *MyClass_new(PyTypeObject *type, PyObject *args, PyObject *kwds){std::cout << "MtClass_new() called!" << std::endl;MyClassObject *self;self = (MyClassObject*) type->tp_alloc(type, 0);if(self != NULL){ // -> 分配成功// 赋初始值self->m_value = 0;self->m_myclass = NULL; }return (PyObject*) self;}
实际的初始化发生在 MyClass_init
中,它对应于 Python 中的 __init__() 方法:
int MyClass_init(PyObject *self, PyObject *args, PyObject *kwds){((MyClassObject *)self)->m_value = 123;MyClassObject* m = (MyClassObject*)self;m->m_myclass = (MyClass*)PyObject_Malloc(sizeof(MyClass));if(!m->m_myclass){PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed");return -1;}try {new (m->m_myclass) MyClass();} catch (const std::exception& ex) {PyObject_Free(m->m_myclass);m->m_myclass = NULL;m->m_value = 0;PyErr_SetString(PyExc_RuntimeError, ex.what());return -1;} catch(...) {PyObject_Free(m->m_myclass);m->m_myclass = NULL;m->m_value = 0;PyErr_SetString(PyExc_RuntimeError, "Initialization failed");return -1;}return 0;}
如果你想在初始化过程中传递参数,必须在此时调用 PyArg_ParseTuple。简单起见,本例将忽略初始化过程中传递的所有参数。在函数的第一部分中,PyObject
指针(self
)被强转为 MyClassObject
类型的指针,以便访问其他成员。此外,还分配了 C++ 类的内存,并执行了构造函数。
注意,为了防止内存泄漏,必须仔细执行异常处理和内存分配(还有释放)。当引用计数将为零时,MyClass_dealloc
函数负责释放所有相关的堆内存。在文档中有一个章节专门讲述关于 C 和 C++ 扩展的内存管理。
从 Python 类中调用相关的 C++ 类方法很简单:
PyObject* MyClass_addOne(PyObject *self, PyObject *args){assert(self);MyClassObject* _self = reinterpret_cast<MyClassObject*>(self);unsigned long val = _self->m_myclass->addOne();return PyLong_FromUnsignedLong(val);}
同样,PyObject
参数(self
)被强转为 MyClassObject
类型以便访问 m_myclass
,它指向 C++ 对应类实例的指针。有了这些信息,调用 addOne()
类方法,并且结果以 Python 整数对象 返回。
出于调试目的,在调试配置中编译 CPython 解释器是很有价值的。详细描述参阅 官方文档。只要下载了预安装的解释器的其他调试符号,就可以按照下面的步骤进行操作。
当然,老式的 GNU 调试器(GDB) 也可以派上用场。源码中包含了一个 gdbinit 文件,定义了一些选项和断点,另外还有一个 gdb.sh 脚本,它会创建一个调试构建并启动一个 GDB 会话:
Gnu 调试器(GDB)对于 Python C 和 C++ 扩展非常有用
GDB 使用脚本文件 main.py 调用 CPython 解释器,它允许你轻松定义你想要使用 Python 扩展模块执行的所有操作。
另一种方法是将 CPython 解释器嵌入到一个单独的 C++ 应用程序中。可以在仓库的 pydbg.cpp 文件中找到:
int main(int argc, char *argv[], char *envp[]){Py_SetProgramName(L"DbgPythonCppExtension");Py_Initialize();PyObject *pmodule = PyImport_ImportModule("MyModule");if (!pmodule) {PyErr_Print();std::cerr << "Failed to import module MyModule" << std::endl;return -1;}PyObject *myClassType = PyObject_GetAttrString(pmodule, "MyClass");if (!myClassType) {std::cerr << "Unable to get type MyClass from MyModule" << std::endl;return -1;}PyObject *myClassInstance = PyObject_CallObject(myClassType, NULL);if (!myClassInstance) {std::cerr << "Instantioation of MyClass failed" << std::endl;return -1;}Py_DecRef(myClassInstance); // invoke deallocationreturn 0;}
使用 高级接口,可以导入扩展模块并对其执行操作。它允许你在本地 IDE 环境中进行调试,还能让你更好地控制传递或来自扩展模块的变量。
缺点是创建一个额外的应用程序的成本很高。
使用像 CodeLLDB 这样的调试器扩展可能是最方便的调试选项。仓库包含了一些 VSCode/VSCodium 的配置文件,用于构建扩展,如 task.json、CMake Tools 和调用调试器(launch.json)。这种方法结合了前面几种方法的优点:在图形 IDE 中调试,在 Python 脚本文件中定义操作,甚至在解释器提示符中动态定义操作。
VSCodium 有一个集成的调试器。
Python 的所有功能也可以从 C 或 C++ 扩展中获得。虽然用 Python 写代码通常认为是一件容易的事情,但用 C 或 C++ 扩展 Python 代码是一件痛苦的事情。另一方面,虽然原生 Python 代码比 C++ 慢,但 C 或 C++ 扩展可以将计算密集型任务提升到原生机器码的速度。
你还必须考虑 ABI 的使用。稳定的 ABI 提供了一种方法来保持旧版本 CPython 的向后兼容性,如 文档 所述。
最后,你必须自己权衡利弊。如果你决定使用 C 语言来扩展 Python 中的一些功能,你已经看到了如何实现它。
위 내용은 Python용 C++ 확장 모듈 작성의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!